(function (f) {
    if (typeof exports === 'object' && typeof module !== 'undefined') {
        module.exports = f()
    } else if (typeof define === 'function' && define.amd) {
        define([], f)
    } else {
        var g
        if (typeof window !== 'undefined') {
            g = window
        } else if (typeof global !== 'undefined') {
            g = global
        } else if (typeof self !== 'undefined') {
            g = self
        } else {
            g = this
        }
        g.kurentoUtils = f()
    }
})(function () {
    var define, module, exports
    return (function () {
        function r(e, n, t) {
            function o(i, f) {
                if (!n[i]) {
                    if (!e[i]) {
                        var c = 'function' == typeof require && require
                        if (!f && c) return c(i, !0)
                        if (u) return u(i, !0)
                        var a = new Error('Cannot find module \'' + i + '\'')
                        throw a.code = 'MODULE_NOT_FOUND', a
                    }
                    var p = n[i] = {exports: {}}
                    e[i][0].call(p.exports, function (r) {
                        var n = e[i][1][r]
                        return o(n || r)
                    }, p, p.exports, r, e, n, t)
                }
                return n[i].exports
            }

            for (var u = 'function' == typeof require && require, i = 0; i < t.length; i++) o(t[i])
            return o
        }

        return r
    })()({
        1: [function (require, module, exports) {
            var freeice = require('freeice')
            var inherits = require('inherits')
            var UAParser = require('ua-parser-js')
            var uuidv4 = require('uuid/v4')
            var hark = require('hark')
            var EventEmitter = require('events').EventEmitter
            var recursive = require('merge').recursive.bind(undefined, true)
            var sdpTranslator = require('sdp-translator')
            var logger = typeof window === 'undefined' ? console : window.Logger || console
            try {
                require('kurento-browser-extensions')
            } catch (error) {
                if (typeof getScreenConstraints === 'undefined') {
                    logger.warn('screen sharing is not available')
                    getScreenConstraints = function getScreenConstraints(sendSource, callback) {
                        callback(new Error('This library is not enabled for screen sharing'))
                    }
                }
            }
            var MEDIA_CONSTRAINTS = {
                audio: true,
                video: {
                    width: 640,
                    framerate: 15
                }
            }
            var ua = typeof window !== 'undefined' && window.navigator ? window.navigator.userAgent : ''
            var parser = new UAParser(ua)
            var browser = parser.getBrowser()

            function insertScriptSrcInHtmlDom(scriptSrc) {
                var script = document.createElement('script')
                script.src = scriptSrc
                var ref = document.querySelector('script')
                ref.parentNode.insertBefore(script, ref)
            }

            function importScriptsDependsOnBrowser() {
                if (browser.name === 'IE') {
                    insertScriptSrcInHtmlDom('https://cdn.temasys.io/adapterjs/0.15.x/adapter.debug.js')
                }
            }

            importScriptsDependsOnBrowser()
            var usePlanB = false
            if (browser.name === 'Chrome' || browser.name === 'Chromium') {
                logger.debug(browser.name + ': using SDP PlanB')
                usePlanB = true
            }

            function noop(error) {
                if (error)
                    logger.error(error)
            }

            function trackStop(track) {
                track.stop && track.stop()
            }

            function streamStop(stream) {
                stream.getTracks().forEach(trackStop)
            }

            var dumpSDP = function (description) {
                if (typeof description === 'undefined' || description === null) {
                    return ''
                }
                return 'type: ' + description.type + '\r\n' + description.sdp
            }

            function bufferizeCandidates(pc, onerror) {
                var candidatesQueue = []

                function setSignalingstatechangeAccordingWwebBrowser(functionToExecute, pc) {
                    if (typeof AdapterJS !== 'undefined' && AdapterJS.webrtcDetectedBrowser === 'IE' && AdapterJS.webrtcDetectedVersion >= 9) {
                        pc.onsignalingstatechange = functionToExecute
                    } else {
                        pc.addEventListener('signalingstatechange', functionToExecute)
                    }
                }

                var signalingstatechangeFunction = function () {
                    if (pc.signalingState === 'stable') {
                        while (candidatesQueue.length) {
                            var entry = candidatesQueue.shift()
                            pc.addIceCandidate(entry.candidate, entry.callback, entry.callback)
                        }
                    }
                }
                setSignalingstatechangeAccordingWwebBrowser(signalingstatechangeFunction, pc)
                return function (candidate, callback) {
                    callback = callback || onerror
                    switch (pc.signalingState) {
                        case 'closed':
                            callback(new Error('PeerConnection object is closed'))
                            break
                        case 'stable':
                            if (pc.remoteDescription) {
                                pc.addIceCandidate(candidate, callback, callback)
                                break
                            }
                        default:
                            candidatesQueue.push({
                                candidate: candidate,
                                callback: callback
                            })
                    }
                }
            }

            function removeFIDFromOffer(sdp) {
                var n = sdp.indexOf('a=ssrc-group:FID')
                if (n > 0) {
                    return sdp.slice(0, n)
                } else {
                    return sdp
                }
            }

            function getSimulcastInfo(videoStream) {
                var videoTracks = videoStream.getVideoTracks()
                if (!videoTracks.length) {
                    logger.warn('No video tracks available in the video stream')
                    return ''
                }
                var lines = [
                    'a=x-google-flag:conference',
                    'a=ssrc-group:SIM 1 2 3',
                    'a=ssrc:1 cname:localVideo',
                    'a=ssrc:1 msid:' + videoStream.id + ' ' + videoTracks[0].id,
                    'a=ssrc:1 mslabel:' + videoStream.id,
                    'a=ssrc:1 label:' + videoTracks[0].id,
                    'a=ssrc:2 cname:localVideo',
                    'a=ssrc:2 msid:' + videoStream.id + ' ' + videoTracks[0].id,
                    'a=ssrc:2 mslabel:' + videoStream.id,
                    'a=ssrc:2 label:' + videoTracks[0].id,
                    'a=ssrc:3 cname:localVideo',
                    'a=ssrc:3 msid:' + videoStream.id + ' ' + videoTracks[0].id,
                    'a=ssrc:3 mslabel:' + videoStream.id,
                    'a=ssrc:3 label:' + videoTracks[0].id
                ]
                lines.push('')
                return lines.join('\n')
            }

            function sleep(milliseconds) {
                var start = new Date().getTime()
                for (var i = 0; i < 10000000; i++) {
                    if (new Date().getTime() - start > milliseconds) {
                        break
                    }
                }
            }

            function setIceCandidateAccordingWebBrowser(functionToExecute, pc) {
                if (typeof AdapterJS !== 'undefined' && AdapterJS.webrtcDetectedBrowser === 'IE' && AdapterJS.webrtcDetectedVersion >= 9) {
                    pc.onicecandidate = functionToExecute
                } else {
                    pc.addEventListener('icecandidate', functionToExecute)
                }
            }

            function WebRtcPeer(mode, options, callback) {
                if (!(this instanceof WebRtcPeer)) {
                    return new WebRtcPeer(mode, options, callback)
                }
                WebRtcPeer.super_.call(this)
                if (options instanceof Function) {
                    callback = options
                    options = undefined
                }
                options = options || {}
                callback = (callback || noop).bind(this)
                var self = this
                var localVideo = options.localVideo
                var remoteVideo = options.remoteVideo
                var videoStream = options.videoStream
                var audioStream = options.audioStream
                var mediaConstraints = options.mediaConstraints
                var pc = options.peerConnection
                var sendSource = options.sendSource || 'webcam'
                var dataChannelConfig = options.dataChannelConfig
                var useDataChannels = options.dataChannels || false
                var dataChannel
                var guid = uuidv4()
                var configuration = recursive({iceServers: freeice()}, options.configuration)
                var onicecandidate = options.onicecandidate
                if (onicecandidate)
                    this.on('icecandidate', onicecandidate)
                var oncandidategatheringdone = options.oncandidategatheringdone
                if (oncandidategatheringdone) {
                    this.on('candidategatheringdone', oncandidategatheringdone)
                }
                var simulcast = options.simulcast
                var multistream = options.multistream
                var interop = new sdpTranslator.Interop()
                var candidatesQueueOut = []
                var candidategatheringdone = false
                Object.defineProperties(this, {
                    'peerConnection': {
                        get: function () {
                            return pc
                        }
                    },
                    'id': {
                        value: options.id || guid,
                        writable: false
                    },
                    'remoteVideo': {
                        get: function () {
                            return remoteVideo
                        }
                    },
                    'localVideo': {
                        get: function () {
                            return localVideo
                        }
                    },
                    'dataChannel': {
                        get: function () {
                            return dataChannel
                        }
                    },
                    'currentFrame': {
                        get: function () {
                            if (!remoteVideo)
                                return
                            if (remoteVideo.readyState < remoteVideo.HAVE_CURRENT_DATA)
                                throw new Error('No video stream data available')
                            var canvas = document.createElement('canvas')
                            canvas.width = remoteVideo.videoWidth
                            canvas.height = remoteVideo.videoHeight
                            canvas.getContext('2d').drawImage(remoteVideo, 0, 0)
                            return canvas
                        }
                    }
                })
                if (!pc) {
                    pc = new RTCPeerConnection(configuration)
                    if (useDataChannels && !dataChannel) {
                        var dcId = 'WebRtcPeer-' + self.id
                        var dcOptions = undefined
                        if (dataChannelConfig) {
                            dcId = dataChannelConfig.id || dcId
                            dcOptions = dataChannelConfig.options
                        }
                        dataChannel = pc.createDataChannel(dcId, dcOptions)
                        if (dataChannelConfig) {
                            dataChannel.onopen = dataChannelConfig.onopen
                            dataChannel.onclose = dataChannelConfig.onclose
                            dataChannel.onmessage = dataChannelConfig.onmessage
                            dataChannel.onbufferedamountlow = dataChannelConfig.onbufferedamountlow
                            dataChannel.onerror = dataChannelConfig.onerror || noop
                        }
                    }
                }
                if (!pc.getLocalStreams && pc.getSenders) {
                    pc.getLocalStreams = function () {
                        var stream = new MediaStream()
                        pc.getSenders().forEach(function (sender) {
                            stream.addTrack(sender.track)
                        })
                        return [stream]
                    }
                }
                if (!pc.getRemoteStreams && pc.getReceivers) {
                    pc.getRemoteStreams = function () {
                        var stream = new MediaStream()
                        pc.getReceivers().forEach(function (sender) {
                            stream.addTrack(sender.track)
                        })
                        return [stream]
                    }
                }
                var iceCandidateFunction = function (event) {
                    var candidate = event.candidate
                    if (EventEmitter.listenerCount(self, 'icecandidate') || EventEmitter.listenerCount(self, 'candidategatheringdone')) {
                        if (candidate) {
                            var cand
                            if (multistream && usePlanB) {
                                cand = interop.candidateToUnifiedPlan(candidate)
                            } else {
                                cand = candidate
                            }
                            if (typeof AdapterJS === 'undefined') {
                                self.emit('icecandidate', cand)
                            }
                            candidategatheringdone = false
                        } else if (!candidategatheringdone) {
                            if (typeof AdapterJS !== 'undefined' && AdapterJS.webrtcDetectedBrowser === 'IE' && AdapterJS.webrtcDetectedVersion >= 9) {
                                EventEmitter.prototype.emit('candidategatheringdone', cand)
                            } else {
                                self.emit('candidategatheringdone')
                            }
                            candidategatheringdone = true
                        }
                    } else if (!candidategatheringdone) {
                        candidatesQueueOut.push(candidate)
                        if (!candidate)
                            candidategatheringdone = true
                    }
                }
                setIceCandidateAccordingWebBrowser(iceCandidateFunction, pc)
                pc.onaddstream = options.onaddstream
                pc.onnegotiationneeded = options.onnegotiationneeded
                this.on('newListener', function (event, listener) {
                    if (event === 'icecandidate' || event === 'candidategatheringdone') {
                        while (candidatesQueueOut.length) {
                            var candidate = candidatesQueueOut.shift()
                            if (!candidate === (event === 'candidategatheringdone')) {
                                listener(candidate)
                            }
                        }
                    }
                })
                var addIceCandidate = bufferizeCandidates(pc)
                this.addIceCandidate = function (iceCandidate, callback) {
                    var candidate
                    if (multistream && usePlanB) {
                        candidate = interop.candidateToPlanB(iceCandidate)
                    } else {
                        candidate = new RTCIceCandidate(iceCandidate)
                    }
                    logger.debug('Remote ICE candidate received', iceCandidate)
                    callback = (callback || noop).bind(this)
                    addIceCandidate(candidate, callback)
                }
                this.generateOffer = function (callback) {
                    callback = callback.bind(this)
                    if (mode === 'recvonly') {
                        var useAudio = mediaConstraints && typeof mediaConstraints.audio === 'boolean' ? mediaConstraints.audio : true
                        var useVideo = mediaConstraints && typeof mediaConstraints.video === 'boolean' ? mediaConstraints.video : true
                        if (useAudio) {
                            pc.addTransceiver('audio', {direction: 'recvonly'})
                        }
                        if (useVideo) {
                            pc.addTransceiver('video', {direction: 'recvonly'})
                        }
                    } else if (mode === 'sendonly') {
                        pc.getTransceivers().forEach(function (transceiver) {
                            transceiver.direction = 'sendonly'
                        })
                    }
                    if (typeof AdapterJS !== 'undefined' && AdapterJS.webrtcDetectedBrowser === 'IE' && AdapterJS.webrtcDetectedVersion >= 9) {
                        var setLocalDescriptionOnSuccess = function () {
                            sleep(1000)
                            var localDescription = pc.localDescription
                            logger.debug('Local description set\n', localDescription.sdp)
                            if (multistream && usePlanB) {
                                localDescription = interop.toUnifiedPlan(localDescription)
                                logger.debug('offer::origPlanB->UnifiedPlan', dumpSDP(localDescription))
                            }
                            callback(null, localDescription.sdp, self.processAnswer.bind(self))
                        }
                        var createOfferOnSuccess = function (offer) {
                            logger.debug('Created SDP offer')
                            logger.debug('Local description set\n', pc.localDescription)
                            pc.setLocalDescription(offer, setLocalDescriptionOnSuccess, callback)
                        }
                        pc.createOffer(createOfferOnSuccess, callback)
                    } else {
                        pc.createOffer().then(function (offer) {
                            logger.debug('Created SDP offer')
                            offer = mangleSdpToAddSimulcast(offer)
                            return pc.setLocalDescription(offer)
                        }).then(function () {
                            var localDescription = pc.localDescription
                            // logger.debug('Local description set\n', localDescription.sdp)
                            if (multistream && usePlanB) {
                                localDescription = interop.toUnifiedPlan(localDescription)
                                logger.debug('offer::origPlanB->UnifiedPlan', dumpSDP(localDescription))
                            }
                            callback(null, localDescription.sdp, self.processAnswer.bind(self))
                        }).catch(callback)
                    }
                }
                this.getLocalSessionDescriptor = function () {
                    return pc.localDescription
                }
                this.getRemoteSessionDescriptor = function () {
                    return pc.remoteDescription
                }

                function setRemoteVideo() {
                    if (remoteVideo) {
                        remoteVideo.pause()
                        var stream = pc.getRemoteStreams()[0]
                        remoteVideo.srcObject = stream
                        logger.debug('Remote stream:', stream)
                        if (typeof AdapterJS !== 'undefined' && AdapterJS.webrtcDetectedBrowser === 'IE' && AdapterJS.webrtcDetectedVersion >= 9) {
                            remoteVideo = attachMediaStream(remoteVideo, stream)
                        } else {
                            remoteVideo.load()
                        }
                    }
                }

                this.showLocalVideo = function () {
                    localVideo.srcObject = videoStream
                    localVideo.muted = true
                    if (typeof AdapterJS !== 'undefined' && AdapterJS.webrtcDetectedBrowser === 'IE' && AdapterJS.webrtcDetectedVersion >= 9) {
                        localVideo = attachMediaStream(localVideo, videoStream)
                    }
                }
                this.send = function (data) {
                    if (dataChannel && dataChannel.readyState === 'open') {
                        dataChannel.send(data)
                    } else {
                        logger.warn('Trying to send data over a non-existing or closed data channel')
                    }
                }
                this.processAnswer = function (sdpAnswer, callback) {
                    callback = (callback || noop).bind(this)
                    var answer = new RTCSessionDescription({
                        type: 'answer',
                        sdp: sdpAnswer
                    })
                    if (multistream && usePlanB) {
                        var planBAnswer = interop.toPlanB(answer)
                        logger.debug('asnwer::planB', dumpSDP(planBAnswer))
                        answer = planBAnswer
                    }
                    logger.debug('SDP answer received, setting remote description')
                    if (pc.signalingState === 'closed') {
                        return callback('PeerConnection is closed')
                    }
                    pc.setRemoteDescription(answer).then(function () {
                        setRemoteVideo()
                        callback()
                    }, callback)
                }
                this.processOffer = function (sdpOffer, callback) {
                    callback = callback.bind(this)
                    var offer = new RTCSessionDescription({
                        type: 'offer',
                        sdp: sdpOffer
                    })
                    if (multistream && usePlanB) {
                        var planBOffer = interop.toPlanB(offer)
                        logger.debug('offer::planB', dumpSDP(planBOffer))
                        offer = planBOffer
                    }
                    logger.debug('SDP offer received, setting remote description')
                    if (pc.signalingState === 'closed') {
                        return callback('PeerConnection is closed')
                    }
                    pc.setRemoteDescription(offer).then(function () {
                        return setRemoteVideo()
                    }).then(function () {
                        return pc.createAnswer()
                    }).then(function (answer) {
                        answer = mangleSdpToAddSimulcast(answer)
                        logger.debug('Created SDP answer')
                        return pc.setLocalDescription(answer)
                    }).then(function () {
                        var localDescription = pc.localDescription
                        if (multistream && usePlanB) {
                            localDescription = interop.toUnifiedPlan(localDescription)
                            logger.debug('answer::origPlanB->UnifiedPlan', dumpSDP(localDescription))
                        }
                        logger.debug('Local description set\n', localDescription.sdp)
                        callback(null, localDescription.sdp)
                    }).catch(callback)
                }

                function mangleSdpToAddSimulcast(answer) {
                    if (simulcast) {
                        if (browser.name === 'Chrome' || browser.name === 'Chromium') {
                            logger.debug('Adding multicast info')
                            answer = new RTCSessionDescription({
                                'type': answer.type,
                                'sdp': removeFIDFromOffer(answer.sdp) + getSimulcastInfo(videoStream)
                            })
                        } else {
                            logger.warn('Simulcast is only available in Chrome browser.')
                        }
                    }
                    return answer
                }

                function start() {
                    if (pc.signalingState === 'closed') {
                        callback('The peer connection object is in "closed" state. This is most likely due to an invocation of the dispose method before accepting in the dialogue')
                    }
                    if (videoStream && localVideo) {
                        self.showLocalVideo()
                    }
                    if (videoStream) {
                        videoStream.getTracks().forEach(function (track) {
                            pc.addTrack(track, videoStream)
                        })
                    }
                    if (audioStream) {
                        audioStream.getTracks().forEach(function (track) {
                            pc.addTrack(track, audioStream)
                        })
                    }
                    callback()
                }

                if (mode !== 'recvonly' && !videoStream && !audioStream) {
                    function getMedia(constraints) {
                        if (constraints === undefined) {
                            constraints = MEDIA_CONSTRAINTS
                        }
                        if (typeof AdapterJS !== 'undefined' && AdapterJS.webrtcDetectedBrowser === 'IE' && AdapterJS.webrtcDetectedVersion >= 9) {
                            navigator.getUserMedia(constraints, function (stream) {
                                videoStream = stream
                                start()
                            }, callback)
                        } else {
                            navigator.mediaDevices.getUserMedia(constraints).then(function (stream) {
                                videoStream = stream
                                start()
                            }).catch(callback)
                        }
                    }

                    if (sendSource === 'webcam') {
                        getMedia(mediaConstraints)
                    } else {
                        getScreenConstraints(sendSource, function (error, constraints_) {
                            if (error)
                                return callback(error)
                            constraints = [mediaConstraints]
                            constraints.unshift(constraints_)
                            getMedia(recursive.apply(undefined, constraints))
                        }, guid)
                    }
                } else {
                    setTimeout(start, 0)
                }
                this.on('_dispose', function () {
                    if (localVideo) {
                        localVideo.pause()
                        localVideo.srcObject = null
                        if (typeof AdapterJS === 'undefined') {
                            localVideo.load()
                        }
                        localVideo.muted = false
                    }
                    if (remoteVideo) {
                        remoteVideo.pause()
                        remoteVideo.srcObject = null
                        if (typeof AdapterJS === 'undefined') {
                            remoteVideo.load()
                        }
                    }
                    self.removeAllListeners()
                    if (typeof window !== 'undefined' && window.cancelChooseDesktopMedia !== undefined) {
                        window.cancelChooseDesktopMedia(guid)
                    }
                })
            }

            inherits(WebRtcPeer, EventEmitter)

            function createEnableDescriptor(type) {
                var method = 'get' + type + 'Tracks'
                return {
                    enumerable: true,
                    get: function () {
                        if (!this.peerConnection)
                            return
                        var streams = this.peerConnection.getLocalStreams()
                        if (!streams.length)
                            return
                        for (var i = 0, stream; stream = streams[i]; i++) {
                            var tracks = stream[method]()
                            for (var j = 0, track; track = tracks[j]; j++)
                                if (!track.enabled)
                                    return false
                        }
                        return true
                    },
                    set: function (value) {
                        function trackSetEnable(track) {
                            track.enabled = value
                        }

                        this.peerConnection.getLocalStreams().forEach(function (stream) {
                            stream[method]().forEach(trackSetEnable)
                        })
                    }
                }
            }

            Object.defineProperties(WebRtcPeer.prototype, {
                'enabled': {
                    enumerable: true,
                    get: function () {
                        return this.audioEnabled && this.videoEnabled
                    },
                    set: function (value) {
                        this.audioEnabled = this.videoEnabled = value
                    }
                },
                'audioEnabled': createEnableDescriptor('Audio'),
                'videoEnabled': createEnableDescriptor('Video')
            })
            WebRtcPeer.prototype.getLocalStream = function (index) {
                if (this.peerConnection) {
                    return this.peerConnection.getLocalStreams()[index || 0]
                }
            }
            WebRtcPeer.prototype.getRemoteStream = function (index) {
                if (this.peerConnection) {
                    return this.peerConnection.getRemoteStreams()[index || 0]
                }
            }
            WebRtcPeer.prototype.dispose = function () {
                logger.debug('Disposing WebRtcPeer')
                var pc = this.peerConnection
                var dc = this.dataChannel
                try {
                    if (dc) {
                        if (dc.readyState === 'closed')
                            return
                        dc.close()
                    }
                    if (pc) {
                        if (pc.signalingState === 'closed')
                            return
                        pc.getLocalStreams().forEach(streamStop)
                        pc.close()
                    }
                } catch (err) {
                    logger.warn('Exception disposing webrtc peer ' + err)
                }
                if (typeof AdapterJS === 'undefined') {
                    this.emit('_dispose')
                }
            }

            function WebRtcPeerRecvonly(options, callback) {
                if (!(this instanceof WebRtcPeerRecvonly)) {
                    return new WebRtcPeerRecvonly(options, callback)
                }
                WebRtcPeerRecvonly.super_.call(this, 'recvonly', options, callback)
            }

            inherits(WebRtcPeerRecvonly, WebRtcPeer)

            function WebRtcPeerSendonly(options, callback) {
                if (!(this instanceof WebRtcPeerSendonly)) {
                    return new WebRtcPeerSendonly(options, callback)
                }
                WebRtcPeerSendonly.super_.call(this, 'sendonly', options, callback)
            }

            inherits(WebRtcPeerSendonly, WebRtcPeer)

            function WebRtcPeerSendrecv(options, callback) {
                if (!(this instanceof WebRtcPeerSendrecv)) {
                    return new WebRtcPeerSendrecv(options, callback)
                }
                WebRtcPeerSendrecv.super_.call(this, 'sendrecv', options, callback)
            }

            inherits(WebRtcPeerSendrecv, WebRtcPeer)

            function harkUtils(stream, options) {
                return hark(stream, options)
            }

            exports.bufferizeCandidates = bufferizeCandidates
            exports.WebRtcPeerRecvonly = WebRtcPeerRecvonly
            exports.WebRtcPeerSendonly = WebRtcPeerSendonly
            exports.WebRtcPeerSendrecv = WebRtcPeerSendrecv
            exports.hark = harkUtils
        }, {
            'events': 4,
            'freeice': 5,
            'hark': 8,
            'inherits': 9,
            'kurento-browser-extensions': 10,
            'merge': 11,
            'sdp-translator': 18,
            'ua-parser-js': 21,
            'uuid/v4': 24
        }],
        2: [function (require, module, exports) {
            if (window.addEventListener)
                module.exports = require('./index')
        }, {'./index': 3}],
        3: [function (require, module, exports) {
            var WebRtcPeer = require('./WebRtcPeer')
            exports.WebRtcPeer = WebRtcPeer
        }, {'./WebRtcPeer': 1}],
        4: [function (require, module, exports) {
// Copyright Joyent, Inc. and other Node contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit
// persons to whom the Software is furnished to do so, subject to the
// following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.

            var objectCreate = Object.create || objectCreatePolyfill
            var objectKeys = Object.keys || objectKeysPolyfill
            var bind = Function.prototype.bind || functionBindPolyfill

            function EventEmitter() {
                if (!this._events || !Object.prototype.hasOwnProperty.call(this, '_events')) {
                    this._events = objectCreate(null)
                    this._eventsCount = 0
                }

                this._maxListeners = this._maxListeners || undefined
            }

            module.exports = EventEmitter

// Backwards-compat with node 0.10.x
            EventEmitter.EventEmitter = EventEmitter

            EventEmitter.prototype._events = undefined
            EventEmitter.prototype._maxListeners = undefined

// By default EventEmitters will print a warning if more than 10 listeners are
// added to it. This is a useful default which helps finding memory leaks.
            var defaultMaxListeners = 10

            var hasDefineProperty
            try {
                var o = {}
                if (Object.defineProperty) Object.defineProperty(o, 'x', {value: 0})
                hasDefineProperty = o.x === 0
            } catch (err) {
                hasDefineProperty = false
            }
            if (hasDefineProperty) {
                Object.defineProperty(EventEmitter, 'defaultMaxListeners', {
                    enumerable: true,
                    get: function () {
                        return defaultMaxListeners
                    },
                    set: function (arg) {
                        // check whether the input is a positive number (whose value is zero or
                        // greater and not a NaN).
                        if (typeof arg !== 'number' || arg < 0 || arg !== arg)
                            throw new TypeError('"defaultMaxListeners" must be a positive number')
                        defaultMaxListeners = arg
                    }
                })
            } else {
                EventEmitter.defaultMaxListeners = defaultMaxListeners
            }

// Obviously not all Emitters should be limited to 10. This function allows
// that to be increased. Set to zero for unlimited.
            EventEmitter.prototype.setMaxListeners = function setMaxListeners(n) {
                if (typeof n !== 'number' || n < 0 || isNaN(n))
                    throw new TypeError('"n" argument must be a positive number')
                this._maxListeners = n
                return this
            }

            function $getMaxListeners(that) {
                if (that._maxListeners === undefined)
                    return EventEmitter.defaultMaxListeners
                return that._maxListeners
            }

            EventEmitter.prototype.getMaxListeners = function getMaxListeners() {
                return $getMaxListeners(this)
            }

// These standalone emit* functions are used to optimize calling of event
// handlers for fast cases because emit() itself often has a variable number of
// arguments and can be deoptimized because of that. These functions always have
// the same number of arguments and thus do not get deoptimized, so the code
// inside them can execute faster.
            function emitNone(handler, isFn, self) {
                if (isFn)
                    handler.call(self)
                else {
                    var len = handler.length
                    var listeners = arrayClone(handler, len)
                    for (var i = 0; i < len; ++i)
                        listeners[i].call(self)
                }
            }

            function emitOne(handler, isFn, self, arg1) {
                if (isFn)
                    handler.call(self, arg1)
                else {
                    var len = handler.length
                    var listeners = arrayClone(handler, len)
                    for (var i = 0; i < len; ++i)
                        listeners[i].call(self, arg1)
                }
            }

            function emitTwo(handler, isFn, self, arg1, arg2) {
                if (isFn)
                    handler.call(self, arg1, arg2)
                else {
                    var len = handler.length
                    var listeners = arrayClone(handler, len)
                    for (var i = 0; i < len; ++i)
                        listeners[i].call(self, arg1, arg2)
                }
            }

            function emitThree(handler, isFn, self, arg1, arg2, arg3) {
                if (isFn)
                    handler.call(self, arg1, arg2, arg3)
                else {
                    var len = handler.length
                    var listeners = arrayClone(handler, len)
                    for (var i = 0; i < len; ++i)
                        listeners[i].call(self, arg1, arg2, arg3)
                }
            }

            function emitMany(handler, isFn, self, args) {
                if (isFn)
                    handler.apply(self, args)
                else {
                    var len = handler.length
                    var listeners = arrayClone(handler, len)
                    for (var i = 0; i < len; ++i)
                        listeners[i].apply(self, args)
                }
            }

            EventEmitter.prototype.emit = function emit(type) {
                var er, handler, len, args, i, events
                var doError = (type === 'error')

                events = this._events
                if (events)
                    doError = (doError && events.error == null)
                else if (!doError)
                    return false

                // If there is no 'error' event listener then throw.
                if (doError) {
                    if (arguments.length > 1)
                        er = arguments[1]
                    if (er instanceof Error) {
                        throw er // Unhandled 'error' event
                    } else {
                        // At least give some kind of context to the user
                        var err = new Error('Unhandled "error" event. (' + er + ')')
                        err.context = er
                        throw err
                    }
                    return false
                }

                handler = events[type]

                if (!handler)
                    return false

                var isFn = typeof handler === 'function'
                len = arguments.length
                switch (len) {
                    // fast cases
                    case 1:
                        emitNone(handler, isFn, this)
                        break
                    case 2:
                        emitOne(handler, isFn, this, arguments[1])
                        break
                    case 3:
                        emitTwo(handler, isFn, this, arguments[1], arguments[2])
                        break
                    case 4:
                        emitThree(handler, isFn, this, arguments[1], arguments[2], arguments[3])
                        break
                    // slower
                    default:
                        args = new Array(len - 1)
                        for (i = 1; i < len; i++)
                            args[i - 1] = arguments[i]
                        emitMany(handler, isFn, this, args)
                }

                return true
            }

            function _addListener(target, type, listener, prepend) {
                var m
                var events
                var existing

                if (typeof listener !== 'function')
                    throw new TypeError('"listener" argument must be a function')

                events = target._events
                if (!events) {
                    events = target._events = objectCreate(null)
                    target._eventsCount = 0
                } else {
                    // To avoid recursion in the case that type === "newListener"! Before
                    // adding it to the listeners, first emit "newListener".
                    if (events.newListener) {
                        target.emit('newListener', type,
                            listener.listener ? listener.listener : listener)

                        // Re-assign `events` because a newListener handler could have caused the
                        // this._events to be assigned to a new object
                        events = target._events
                    }
                    existing = events[type]
                }

                if (!existing) {
                    // Optimize the case of one listener. Don't need the extra array object.
                    existing = events[type] = listener
                    ++target._eventsCount
                } else {
                    if (typeof existing === 'function') {
                        // Adding the second element, need to change to array.
                        existing = events[type] =
                            prepend ? [listener, existing] : [existing, listener]
                    } else {
                        // If we've already got an array, just append.
                        if (prepend) {
                            existing.unshift(listener)
                        } else {
                            existing.push(listener)
                        }
                    }

                    // Check for listener leak
                    if (!existing.warned) {
                        m = $getMaxListeners(target)
                        if (m && m > 0 && existing.length > m) {
                            existing.warned = true
                            var w = new Error('Possible EventEmitter memory leak detected. ' +
                                existing.length + ' "' + String(type) + '" listeners ' +
                                'added. Use emitter.setMaxListeners() to ' +
                                'increase limit.')
                            w.name = 'MaxListenersExceededWarning'
                            w.emitter = target
                            w.type = type
                            w.count = existing.length
                            if (typeof console === 'object' && console.warn) {
                                console.warn('%s: %s', w.name, w.message)
                            }
                        }
                    }
                }

                return target
            }

            EventEmitter.prototype.addListener = function addListener(type, listener) {
                return _addListener(this, type, listener, false)
            }

            EventEmitter.prototype.on = EventEmitter.prototype.addListener

            EventEmitter.prototype.prependListener =
                function prependListener(type, listener) {
                    return _addListener(this, type, listener, true)
                }

            function onceWrapper() {
                if (!this.fired) {
                    this.target.removeListener(this.type, this.wrapFn)
                    this.fired = true
                    switch (arguments.length) {
                        case 0:
                            return this.listener.call(this.target)
                        case 1:
                            return this.listener.call(this.target, arguments[0])
                        case 2:
                            return this.listener.call(this.target, arguments[0], arguments[1])
                        case 3:
                            return this.listener.call(this.target, arguments[0], arguments[1],
                                arguments[2])
                        default:
                            var args = new Array(arguments.length)
                            for (var i = 0; i < args.length; ++i)
                                args[i] = arguments[i]
                            this.listener.apply(this.target, args)
                    }
                }
            }

            function _onceWrap(target, type, listener) {
                var state = {fired: false, wrapFn: undefined, target: target, type: type, listener: listener}
                var wrapped = bind.call(onceWrapper, state)
                wrapped.listener = listener
                state.wrapFn = wrapped
                return wrapped
            }

            EventEmitter.prototype.once = function once(type, listener) {
                if (typeof listener !== 'function')
                    throw new TypeError('"listener" argument must be a function')
                this.on(type, _onceWrap(this, type, listener))
                return this
            }

            EventEmitter.prototype.prependOnceListener =
                function prependOnceListener(type, listener) {
                    if (typeof listener !== 'function')
                        throw new TypeError('"listener" argument must be a function')
                    this.prependListener(type, _onceWrap(this, type, listener))
                    return this
                }

// Emits a 'removeListener' event if and only if the listener was removed.
            EventEmitter.prototype.removeListener =
                function removeListener(type, listener) {
                    var list, events, position, i, originalListener

                    if (typeof listener !== 'function')
                        throw new TypeError('"listener" argument must be a function')

                    events = this._events
                    if (!events)
                        return this

                    list = events[type]
                    if (!list)
                        return this

                    if (list === listener || list.listener === listener) {
                        if (--this._eventsCount === 0)
                            this._events = objectCreate(null)
                        else {
                            delete events[type]
                            if (events.removeListener)
                                this.emit('removeListener', type, list.listener || listener)
                        }
                    } else if (typeof list !== 'function') {
                        position = -1

                        for (i = list.length - 1; i >= 0; i--) {
                            if (list[i] === listener || list[i].listener === listener) {
                                originalListener = list[i].listener
                                position = i
                                break
                            }
                        }

                        if (position < 0)
                            return this

                        if (position === 0)
                            list.shift()
                        else
                            spliceOne(list, position)

                        if (list.length === 1)
                            events[type] = list[0]

                        if (events.removeListener)
                            this.emit('removeListener', type, originalListener || listener)
                    }

                    return this
                }

            EventEmitter.prototype.removeAllListeners =
                function removeAllListeners(type) {
                    var listeners, events, i

                    events = this._events
                    if (!events)
                        return this

                    // not listening for removeListener, no need to emit
                    if (!events.removeListener) {
                        if (arguments.length === 0) {
                            this._events = objectCreate(null)
                            this._eventsCount = 0
                        } else if (events[type]) {
                            if (--this._eventsCount === 0)
                                this._events = objectCreate(null)
                            else
                                delete events[type]
                        }
                        return this
                    }

                    // emit removeListener for all listeners on all events
                    if (arguments.length === 0) {
                        var keys = objectKeys(events)
                        var key
                        for (i = 0; i < keys.length; ++i) {
                            key = keys[i]
                            if (key === 'removeListener') continue
                            this.removeAllListeners(key)
                        }
                        this.removeAllListeners('removeListener')
                        this._events = objectCreate(null)
                        this._eventsCount = 0
                        return this
                    }

                    listeners = events[type]

                    if (typeof listeners === 'function') {
                        this.removeListener(type, listeners)
                    } else if (listeners) {
                        // LIFO order
                        for (i = listeners.length - 1; i >= 0; i--) {
                            this.removeListener(type, listeners[i])
                        }
                    }

                    return this
                }

            function _listeners(target, type, unwrap) {
                var events = target._events

                if (!events)
                    return []

                var evlistener = events[type]
                if (!evlistener)
                    return []

                if (typeof evlistener === 'function')
                    return unwrap ? [evlistener.listener || evlistener] : [evlistener]

                return unwrap ? unwrapListeners(evlistener) : arrayClone(evlistener, evlistener.length)
            }

            EventEmitter.prototype.listeners = function listeners(type) {
                return _listeners(this, type, true)
            }

            EventEmitter.prototype.rawListeners = function rawListeners(type) {
                return _listeners(this, type, false)
            }

            EventEmitter.listenerCount = function (emitter, type) {
                if (typeof emitter.listenerCount === 'function') {
                    return emitter.listenerCount(type)
                } else {
                    return listenerCount.call(emitter, type)
                }
            }

            EventEmitter.prototype.listenerCount = listenerCount

            function listenerCount(type) {
                var events = this._events

                if (events) {
                    var evlistener = events[type]

                    if (typeof evlistener === 'function') {
                        return 1
                    } else if (evlistener) {
                        return evlistener.length
                    }
                }

                return 0
            }

            EventEmitter.prototype.eventNames = function eventNames() {
                return this._eventsCount > 0 ? Reflect.ownKeys(this._events) : []
            }

// About 1.5x faster than the two-arg version of Array#splice().
            function spliceOne(list, index) {
                for (var i = index, k = i + 1, n = list.length; k < n; i += 1, k += 1)
                    list[i] = list[k]
                list.pop()
            }

            function arrayClone(arr, n) {
                var copy = new Array(n)
                for (var i = 0; i < n; ++i)
                    copy[i] = arr[i]
                return copy
            }

            function unwrapListeners(arr) {
                var ret = new Array(arr.length)
                for (var i = 0; i < ret.length; ++i) {
                    ret[i] = arr[i].listener || arr[i]
                }
                return ret
            }

            function objectCreatePolyfill(proto) {
                var F = function () {
                }
                F.prototype = proto
                return new F
            }

            function objectKeysPolyfill(obj) {
                var keys = []
                for (var k in obj) if (Object.prototype.hasOwnProperty.call(obj, k)) {
                    keys.push(k)
                }
                return k
            }

            function functionBindPolyfill(context) {
                var fn = this
                return function () {
                    return fn.apply(context, arguments)
                }
            }

        }, {}],
        5: [function (require, module, exports) {
            /* jshint node: true */
            'use strict'

            var normalice = require('normalice')

            /**
             # freeice

             The `freeice` module is a simple way of getting random STUN or TURN server
             for your WebRTC application.  The list of servers (just STUN at this stage)
             were sourced from this [gist](https://gist.github.com/zziuni/3741933).

             ## Example Use

             The following demonstrates how you can use `freeice` with
             [rtc-quickconnect](https://github.com/rtc-io/rtc-quickconnect):

             <<< examples/quickconnect.js

             As the `freeice` module generates ice servers in a list compliant with the
             WebRTC spec you will be able to use it with raw `RTCPeerConnection`
             constructors and other WebRTC libraries.

             ## Hey, don't use my STUN/TURN server!

             If for some reason your free STUN or TURN server ends up in the
             list of servers ([stun](https://github.com/DamonOehlman/freeice/blob/master/stun.json) or
             [turn](https://github.com/DamonOehlman/freeice/blob/master/turn.json))
             that is used in this module, you can feel
             free to open an issue on this repository and those servers will be removed
             within 24 hours (or sooner).  This is the quickest and probably the most
             polite way to have something removed (and provides us some visibility
             if someone opens a pull request requesting that a server is added).

             ## Please add my server!

             If you have a server that you wish to add to the list, that's awesome! I'm
             sure I speak on behalf of a whole pile of WebRTC developers who say thanks.
             To get it into the list, feel free to either open a pull request or if you
             find that process a bit daunting then just create an issue requesting
             the addition of the server (make sure you provide all the details, and if
             you have a Terms of Service then including that in the PR/issue would be
             awesome).

             ## I know of a free server, can I add it?

             Sure, if you do your homework and make sure it is ok to use (I'm currently
             in the process of reviewing the terms of those STUN servers included from
             the original list).  If it's ok to go, then please see the previous entry
             for how to add it.

             ## Current List of Servers

             * current as at the time of last `README.md` file generation

             ### STUN

             <<< stun.json

             ### TURN

             <<< turn.json

             **/

            var freeice = function (opts) {
                // if a list of servers has been provided, then use it instead of defaults
                var servers = {
                    stun: (opts || {}).stun || require('./stun.json'),
                    turn: (opts || {}).turn || require('./turn.json')
                }

                var stunCount = (opts || {}).stunCount || 2
                var turnCount = (opts || {}).turnCount || 0
                var selected

                function getServers(type, count) {
                    var out = []
                    var input = [].concat(servers[type])
                    var idx

                    while (input.length && out.length < count) {
                        idx = (Math.random() * input.length) | 0
                        out = out.concat(input.splice(idx, 1))
                    }

                    return out.map(function (url) {
                        //If it's a not a string, don't try to "normalice" it otherwise using type:url will screw it up
                        if ((typeof url !== 'string') && (!(url instanceof String))) {
                            return url
                        } else {
                            return normalice(type + ':' + url)
                        }
                    })
                }

                // add stun servers
                selected = [].concat(getServers('stun', stunCount))

                if (turnCount) {
                    selected = selected.concat(getServers('turn', turnCount))
                }

                return selected
            }

            module.exports = freeice
        }, {'./stun.json': 6, './turn.json': 7, 'normalice': 12}],
        6: [function (require, module, exports) {
            module.exports = [
                'stun.l.google.com:19302',
                'stun1.l.google.com:19302',
                'stun2.l.google.com:19302',
                'stun3.l.google.com:19302',
                'stun4.l.google.com:19302',
                'stun.ekiga.net',
                'stun.ideasip.com',
                'stun.schlund.de',
                'stun.stunprotocol.org:3478',
                'stun.voiparound.com',
                'stun.voipbuster.com',
                'stun.voipstunt.com',
                'stun.voxgratia.org'
            ]

        }, {}],
        7: [function (require, module, exports) {
            module.exports = []

        }, {}],
        8: [function (require, module, exports) {
            var WildEmitter = require('wildemitter')

            function getMaxVolume(analyser, fftBins) {
                var maxVolume = -Infinity
                analyser.getFloatFrequencyData(fftBins)

                for (var i = 4, ii = fftBins.length; i < ii; i++) {
                    if (fftBins[i] > maxVolume && fftBins[i] < 0) {
                        maxVolume = fftBins[i]
                    }
                }


                return maxVolume
            }


            var audioContextType
            if (typeof window !== 'undefined') {
                audioContextType = window.AudioContext || window.webkitAudioContext
            }
// use a single audio context due to hardware limits
            var audioContext = null
            module.exports = function (stream, options) {
                var harker = new WildEmitter()

                // make it not break in non-supported browsers
                if (!audioContextType) return harker

                //Config
                var options = options || {},
                    smoothing = (options.smoothing || 0.1),
                    interval = (options.interval || 50),
                    threshold = options.threshold,
                    play = options.play,
                    history = options.history || 10,
                    running = true

                // Ensure that just a single AudioContext is internally created
                audioContext = options.audioContext || audioContext || new audioContextType()

                var sourceNode, fftBins, analyser

                analyser = audioContext.createAnalyser()
                analyser.fftSize = 512
                analyser.smoothingTimeConstant = smoothing
                fftBins = new Float32Array(analyser.frequencyBinCount)

                if (stream.jquery) stream = stream[0]
                if (stream instanceof HTMLAudioElement || stream instanceof HTMLVideoElement) {
                    //Audio Tag
                    sourceNode = audioContext.createMediaElementSource(stream)
                    if (typeof play === 'undefined') play = true
                    threshold = threshold || -50
                } else {
                    //WebRTC Stream
                    sourceNode = audioContext.createMediaStreamSource(stream)
                    threshold = threshold || -50
                }

                sourceNode.connect(analyser)
                if (play) analyser.connect(audioContext.destination)

                harker.speaking = false

                harker.suspend = function () {
                    return audioContext.suspend()
                }
                harker.resume = function () {
                    return audioContext.resume()
                }
                Object.defineProperty(harker, 'state', {
                    get: function () {
                        return audioContext.state
                    }
                })
                audioContext.onstatechange = function () {
                    harker.emit('state_change', audioContext.state)
                }

                harker.setThreshold = function (t) {
                    threshold = t
                }

                harker.setInterval = function (i) {
                    interval = i
                }

                harker.stop = function () {
                    running = false
                    harker.emit('volume_change', -100, threshold)
                    if (harker.speaking) {
                        harker.speaking = false
                        harker.emit('stopped_speaking')
                    }
                    analyser.disconnect()
                    sourceNode.disconnect()
                }
                harker.speakingHistory = []
                for (var i = 0; i < history; i++) {
                    harker.speakingHistory.push(0)
                }

                // Poll the analyser node to determine if speaking
                // and emit events if changed
                var looper = function () {
                    setTimeout(function () {

                        //check if stop has been called
                        if (!running) {
                            return
                        }

                        var currentVolume = getMaxVolume(analyser, fftBins)

                        harker.emit('volume_change', currentVolume, threshold)

                        var history = 0
                        if (currentVolume > threshold && !harker.speaking) {
                            // trigger quickly, short history
                            for (var i = harker.speakingHistory.length - 3; i < harker.speakingHistory.length; i++) {
                                history += harker.speakingHistory[i]
                            }
                            if (history >= 2) {
                                harker.speaking = true
                                harker.emit('speaking')
                            }
                        } else if (currentVolume < threshold && harker.speaking) {
                            for (var i = 0; i < harker.speakingHistory.length; i++) {
                                history += harker.speakingHistory[i]
                            }
                            if (history == 0) {
                                harker.speaking = false
                                harker.emit('stopped_speaking')
                            }
                        }
                        harker.speakingHistory.shift()
                        harker.speakingHistory.push(0 + (currentVolume > threshold))

                        looper()
                    }, interval)
                }
                looper()

                return harker
            }

        }, {'wildemitter': 25}],
        9: [function (require, module, exports) {
            if (typeof Object.create === 'function') {
                // implementation from standard node.js 'util' module
                module.exports = function inherits(ctor, superCtor) {
                    if (superCtor) {
                        ctor.super_ = superCtor
                        ctor.prototype = Object.create(superCtor.prototype, {
                            constructor: {
                                value: ctor,
                                enumerable: false,
                                writable: true,
                                configurable: true
                            }
                        })
                    }
                }
            } else {
                // old school shim for old browsers
                module.exports = function inherits(ctor, superCtor) {
                    if (superCtor) {
                        ctor.super_ = superCtor
                        var TempCtor = function () {
                        }
                        TempCtor.prototype = superCtor.prototype
                        ctor.prototype = new TempCtor()
                        ctor.prototype.constructor = ctor
                    }
                }
            }

        }, {}],
        10: [function (require, module, exports) {
// Does nothing at all.

        }, {}],
        11: [function (require, module, exports) {
            /*!
 * @name JavaScript/NodeJS Merge v1.2.1
 * @author yeikos
 * @repository https://github.com/yeikos/js.merge

 * Copyright 2014 yeikos - MIT license
 * https://raw.github.com/yeikos/js.merge/master/LICENSE
 */

            ;(function (isNode) {

                /**
                 * Merge one or more objects
                 * @param bool? clone
                 * @param mixed,... arguments
                 * @return object
                 */

                var Public = function (clone) {

                    return merge(clone === true, false, arguments)

                }, publicName = 'merge'

                /**
                 * Merge two or more objects recursively
                 * @param bool? clone
                 * @param mixed,... arguments
                 * @return object
                 */

                Public.recursive = function (clone) {

                    return merge(clone === true, true, arguments)

                }

                /**
                 * Clone the input removing any reference
                 * @param mixed input
                 * @return mixed
                 */

                Public.clone = function (input) {

                    var output = input,
                        type = typeOf(input),
                        index, size

                    if (type === 'array') {

                        output = []
                        size = input.length

                        for (index = 0; index < size; ++index)

                            output[index] = Public.clone(input[index])

                    } else if (type === 'object') {

                        output = {}

                        for (index in input)

                            output[index] = Public.clone(input[index])

                    }

                    return output

                }

                /**
                 * Merge two objects recursively
                 * @param mixed input
                 * @param mixed extend
                 * @return mixed
                 */

                function merge_recursive(base, extend) {

                    if (typeOf(base) !== 'object')

                        return extend

                    for (var key in extend) {

                        if (typeOf(base[key]) === 'object' && typeOf(extend[key]) === 'object') {

                            base[key] = merge_recursive(base[key], extend[key])

                        } else {

                            base[key] = extend[key]

                        }

                    }

                    return base

                }

                /**
                 * Merge two or more objects
                 * @param bool clone
                 * @param bool recursive
                 * @param array argv
                 * @return object
                 */

                function merge(clone, recursive, argv) {

                    var result = argv[0],
                        size = argv.length

                    if (clone || typeOf(result) !== 'object')

                        result = {}

                    for (var index = 0; index < size; ++index) {

                        var item = argv[index],

                            type = typeOf(item)

                        if (type !== 'object') continue

                        for (var key in item) {

                            if (key === '__proto__') continue

                            var sitem = clone ? Public.clone(item[key]) : item[key]

                            if (recursive) {

                                result[key] = merge_recursive(result[key], sitem)

                            } else {

                                result[key] = sitem

                            }

                        }

                    }

                    return result

                }

                /**
                 * Get type of variable
                 * @param mixed input
                 * @return string
                 *
                 * @see http://jsperf.com/typeofvar
                 */

                function typeOf(input) {

                    return ({}).toString.call(input).slice(8, -1).toLowerCase()

                }

                if (isNode) {

                    module.exports = Public

                } else {

                    window[publicName] = Public

                }

            })(typeof module === 'object' && module && typeof module.exports === 'object' && module.exports)
        }, {}],
        12: [function (require, module, exports) {
            /**
             # normalice

             Normalize an ice server configuration object (or plain old string) into a format
             that is usable in all browsers supporting WebRTC.  Primarily this module is designed
             to help with the transition of the `url` attribute of the configuration object to
             the `urls` attribute.

             ## Example Usage

             <<< examples/simple.js

             **/

            var protocols = [
                'stun:',
                'turn:'
            ]

            module.exports = function (input) {
                var url = (input || {}).url || input
                var protocol
                var parts
                var output = {}

                // if we don't have a string url, then allow the input to passthrough
                if (typeof url != 'string' && (!(url instanceof String))) {
                    return input
                }

                // trim the url string, and convert to an array
                url = url.trim()

                // if the protocol is not known, then passthrough
                protocol = protocols[protocols.indexOf(url.slice(0, 5))]
                if (!protocol) {
                    return input
                }

                // now let's attack the remaining url parts
                url = url.slice(5)
                parts = url.split('@')

                output.username = input.username
                output.credential = input.credential
                // if we have an authentication part, then set the credentials
                if (parts.length > 1) {
                    url = parts[1]
                    parts = parts[0].split(':')

                    // add the output credential and username
                    output.username = parts[0]
                    output.credential = (input || {}).credential || parts[1] || ''
                }

                output.url = protocol + url
                output.urls = [output.url]

                return output
            }

        }, {}],
        13: [function (require, module, exports) {
            var grammar = module.exports = {
                v: [{
                    name: 'version',
                    reg: /^(\d*)$/
                }],
                o: [{ //o=- 20518 0 IN IP4 203.0.113.1
                    // NB: sessionId will be a String in most cases because it is huge
                    name: 'origin',
                    reg: /^(\S*) (\d*) (\d*) (\S*) IP(\d) (\S*)/,
                    names: ['username', 'sessionId', 'sessionVersion', 'netType', 'ipVer', 'address'],
                    format: '%s %s %d %s IP%d %s'
                }],
                // default parsing of these only (though some of these feel outdated)
                s: [{name: 'name'}],
                i: [{name: 'description'}],
                u: [{name: 'uri'}],
                e: [{name: 'email'}],
                p: [{name: 'phone'}],
                z: [{name: 'timezones'}], // TODO: this one can actually be parsed properly..
                r: [{name: 'repeats'}],   // TODO: this one can also be parsed properly
                //k: [{}], // outdated thing ignored
                t: [{ //t=0 0
                    name: 'timing',
                    reg: /^(\d*) (\d*)/,
                    names: ['start', 'stop'],
                    format: '%d %d'
                }],
                c: [{ //c=IN IP4 10.47.197.26
                    name: 'connection',
                    reg: /^IN IP(\d) (\S*)/,
                    names: ['version', 'ip'],
                    format: 'IN IP%d %s'
                }],
                b: [{ //b=AS:4000
                    push: 'bandwidth',
                    reg: /^(TIAS|AS|CT|RR|RS):(\d*)/,
                    names: ['type', 'limit'],
                    format: '%s:%s'
                }],
                m: [{ //m=video 51744 RTP/AVP 126 97 98 34 31
                    // NB: special - pushes to session
                    // TODO: rtp/fmtp should be filtered by the payloads found here?
                    reg: /^(\w*) (\d*) ([\w\/]*)(?: (.*))?/,
                    names: ['type', 'port', 'protocol', 'payloads'],
                    format: '%s %d %s %s'
                }],
                a: [
                    { //a=rtpmap:110 opus/48000/2
                        push: 'rtp',
                        reg: /^rtpmap:(\d*) ([\w\-]*)(?:\s*\/(\d*)(?:\s*\/(\S*))?)?/,
                        names: ['payload', 'codec', 'rate', 'encoding'],
                        format: function (o) {
                            return (o.encoding) ?
                                'rtpmap:%d %s/%s/%s' :
                                o.rate ?
                                    'rtpmap:%d %s/%s' :
                                    'rtpmap:%d %s'
                        }
                    },
                    {
                        //a=fmtp:108 profile-level-id=24;object=23;bitrate=64000
                        //a=fmtp:111 minptime=10; useinbandfec=1
                        push: 'fmtp',
                        reg: /^fmtp:(\d*) ([\S| ]*)/,
                        names: ['payload', 'config'],
                        format: 'fmtp:%d %s'
                    },
                    { //a=control:streamid=0
                        name: 'control',
                        reg: /^control:(.*)/,
                        format: 'control:%s'
                    },
                    { //a=rtcp:65179 IN IP4 193.84.77.194
                        name: 'rtcp',
                        reg: /^rtcp:(\d*)(?: (\S*) IP(\d) (\S*))?/,
                        names: ['port', 'netType', 'ipVer', 'address'],
                        format: function (o) {
                            return (o.address != null) ?
                                'rtcp:%d %s IP%d %s' :
                                'rtcp:%d'
                        }
                    },
                    { //a=rtcp-fb:98 trr-int 100
                        push: 'rtcpFbTrrInt',
                        reg: /^rtcp-fb:(\*|\d*) trr-int (\d*)/,
                        names: ['payload', 'value'],
                        format: 'rtcp-fb:%d trr-int %d'
                    },
                    { //a=rtcp-fb:98 nack rpsi
                        push: 'rtcpFb',
                        reg: /^rtcp-fb:(\*|\d*) ([\w-_]*)(?: ([\w-_]*))?/,
                        names: ['payload', 'type', 'subtype'],
                        format: function (o) {
                            return (o.subtype != null) ?
                                'rtcp-fb:%s %s %s' :
                                'rtcp-fb:%s %s'
                        }
                    },
                    { //a=extmap:2 urn:ietf:params:rtp-hdrext:toffset
                        //a=extmap:1/recvonly URI-gps-string
                        push: 'ext',
                        reg: /^extmap:([\w_\/]*) (\S*)(?: (\S*))?/,
                        names: ['value', 'uri', 'config'], // value may include "/direction" suffix
                        format: function (o) {
                            return (o.config != null) ?
                                'extmap:%s %s %s' :
                                'extmap:%s %s'
                        }
                    },
                    {
                        //a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:PS1uQCVeeCFCanVmcjkpPywjNWhcYD0mXXtxaVBR|2^20|1:32
                        push: 'crypto',
                        reg: /^crypto:(\d*) ([\w_]*) (\S*)(?: (\S*))?/,
                        names: ['id', 'suite', 'config', 'sessionConfig'],
                        format: function (o) {
                            return (o.sessionConfig != null) ?
                                'crypto:%d %s %s %s' :
                                'crypto:%d %s %s'
                        }
                    },
                    { //a=setup:actpass
                        name: 'setup',
                        reg: /^setup:(\w*)/,
                        format: 'setup:%s'
                    },
                    { //a=mid:1
                        name: 'mid',
                        reg: /^mid:([^\s]*)/,
                        format: 'mid:%s'
                    },
                    { //a=msid:0c8b064d-d807-43b4-b434-f92a889d8587 98178685-d409-46e0-8e16-7ef0db0db64a
                        name: 'msid',
                        reg: /^msid:(.*)/,
                        format: 'msid:%s'
                    },
                    { //a=ptime:20
                        name: 'ptime',
                        reg: /^ptime:(\d*)/,
                        format: 'ptime:%d'
                    },
                    { //a=maxptime:60
                        name: 'maxptime',
                        reg: /^maxptime:(\d*)/,
                        format: 'maxptime:%d'
                    },
                    { //a=sendrecv
                        name: 'direction',
                        reg: /^(sendrecv|recvonly|sendonly|inactive)/
                    },
                    { //a=ice-lite
                        name: 'icelite',
                        reg: /^(ice-lite)/
                    },
                    { //a=ice-ufrag:F7gI
                        name: 'iceUfrag',
                        reg: /^ice-ufrag:(\S*)/,
                        format: 'ice-ufrag:%s'
                    },
                    { //a=ice-pwd:x9cml/YzichV2+XlhiMu8g
                        name: 'icePwd',
                        reg: /^ice-pwd:(\S*)/,
                        format: 'ice-pwd:%s'
                    },
                    { //a=fingerprint:SHA-1 00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF:00:11:22:33
                        name: 'fingerprint',
                        reg: /^fingerprint:(\S*) (\S*)/,
                        names: ['type', 'hash'],
                        format: 'fingerprint:%s %s'
                    },
                    {
                        //a=candidate:0 1 UDP 2113667327 203.0.113.1 54400 typ host
                        //a=candidate:1162875081 1 udp 2113937151 192.168.34.75 60017 typ host generation 0
                        //a=candidate:3289912957 2 udp 1845501695 193.84.77.194 60017 typ srflx raddr 192.168.34.75 rport 60017 generation 0
                        //a=candidate:229815620 1 tcp 1518280447 192.168.150.19 60017 typ host tcptype active generation 0
                        //a=candidate:3289912957 2 tcp 1845501695 193.84.77.194 60017 typ srflx raddr 192.168.34.75 rport 60017 tcptype passive generation 0
                        push: 'candidates',
                        reg: /^candidate:(\S*) (\d*) (\S*) (\d*) (\S*) (\d*) typ (\S*)(?: raddr (\S*) rport (\d*))?(?: tcptype (\S*))?(?: generation (\d*))?/,
                        names: ['foundation', 'component', 'transport', 'priority', 'ip', 'port', 'type', 'raddr', 'rport', 'tcptype', 'generation'],
                        format: function (o) {
                            var str = 'candidate:%s %d %s %d %s %d typ %s'

                            str += (o.raddr != null) ? ' raddr %s rport %d' : '%v%v'

                            // NB: candidate has three optional chunks, so %void middles one if it's missing
                            str += (o.tcptype != null) ? ' tcptype %s' : '%v'

                            if (o.generation != null) {
                                str += ' generation %d'
                            }
                            return str
                        }
                    },
                    { //a=end-of-candidates (keep after the candidates line for readability)
                        name: 'endOfCandidates',
                        reg: /^(end-of-candidates)/
                    },
                    { //a=remote-candidates:1 203.0.113.1 54400 2 203.0.113.1 54401 ...
                        name: 'remoteCandidates',
                        reg: /^remote-candidates:(.*)/,
                        format: 'remote-candidates:%s'
                    },
                    { //a=ice-options:google-ice
                        name: 'iceOptions',
                        reg: /^ice-options:(\S*)/,
                        format: 'ice-options:%s'
                    },
                    { //a=ssrc:2566107569 cname:t9YU8M1UxTF8Y1A1
                        push: 'ssrcs',
                        reg: /^ssrc:(\d*) ([\w_]*):(.*)/,
                        names: ['id', 'attribute', 'value'],
                        format: 'ssrc:%d %s:%s'
                    },
                    { //a=ssrc-group:FEC 1 2
                        push: 'ssrcGroups',
                        reg: /^ssrc-group:(\w*) (.*)/,
                        names: ['semantics', 'ssrcs'],
                        format: 'ssrc-group:%s %s'
                    },
                    { //a=msid-semantic: WMS Jvlam5X3SX1OP6pn20zWogvaKJz5Hjf9OnlV
                        name: 'msidSemantic',
                        reg: /^msid-semantic:\s?(\w*) (\S*)/,
                        names: ['semantic', 'token'],
                        format: 'msid-semantic: %s %s' // space after ":" is not accidental
                    },
                    { //a=group:BUNDLE audio video
                        push: 'groups',
                        reg: /^group:(\w*) (.*)/,
                        names: ['type', 'mids'],
                        format: 'group:%s %s'
                    },
                    { //a=rtcp-mux
                        name: 'rtcpMux',
                        reg: /^(rtcp-mux)/
                    },
                    { //a=rtcp-rsize
                        name: 'rtcpRsize',
                        reg: /^(rtcp-rsize)/
                    },
                    { // any a= that we don't understand is kepts verbatim on media.invalid
                        push: 'invalid',
                        names: ['value']
                    }
                ]
            }

// set sensible defaults to avoid polluting the grammar with boring details
            Object.keys(grammar).forEach(function (key) {
                var objs = grammar[key]
                objs.forEach(function (obj) {
                    if (!obj.reg) {
                        obj.reg = /(.*)/
                    }
                    if (!obj.format) {
                        obj.format = '%s'
                    }
                })
            })

        }, {}],
        14: [function (require, module, exports) {
            var parser = require('./parser')
            var writer = require('./writer')

            exports.write = writer
            exports.parse = parser.parse
            exports.parseFmtpConfig = parser.parseFmtpConfig
            exports.parsePayloads = parser.parsePayloads
            exports.parseRemoteCandidates = parser.parseRemoteCandidates

        }, {'./parser': 15, './writer': 16}],
        15: [function (require, module, exports) {
            var toIntIfInt = function (v) {
                return String(Number(v)) === v ? Number(v) : v
            }

            var attachProperties = function (match, location, names, rawName) {
                if (rawName && !names) {
                    location[rawName] = toIntIfInt(match[1])
                } else {
                    for (var i = 0; i < names.length; i += 1) {
                        if (match[i + 1] != null) {
                            location[names[i]] = toIntIfInt(match[i + 1])
                        }
                    }
                }
            }

            var parseReg = function (obj, location, content) {
                var needsBlank = obj.name && obj.names
                if (obj.push && !location[obj.push]) {
                    location[obj.push] = []
                } else if (needsBlank && !location[obj.name]) {
                    location[obj.name] = {}
                }
                var keyLocation = obj.push ?
                    {} :  // blank object that will be pushed
                    needsBlank ? location[obj.name] : location // otherwise, named location or root

                attachProperties(content.match(obj.reg), keyLocation, obj.names, obj.name)

                if (obj.push) {
                    location[obj.push].push(keyLocation)
                }
            }

            var grammar = require('./grammar')
            var validLine = RegExp.prototype.test.bind(/^([a-z])=(.*)/)

            exports.parse = function (sdp) {
                var session = {}
                    , media = []
                    , location = session // points at where properties go under (one of the above)

                // parse lines we understand
                sdp.split(/(\r\n|\r|\n)/).filter(validLine).forEach(function (l) {
                    var type = l[0]
                    var content = l.slice(2)
                    if (type === 'm') {
                        media.push({rtp: [], fmtp: []})
                        location = media[media.length - 1] // point at latest media line
                    }

                    for (var j = 0; j < (grammar[type] || []).length; j += 1) {
                        var obj = grammar[type][j]
                        if (obj.reg.test(content)) {
                            return parseReg(obj, location, content)
                        }
                    }
                })

                session.media = media // link it up
                return session
            }

            var fmtpReducer = function (acc, expr) {
                var s = expr.split('=')
                if (s.length === 2) {
                    acc[s[0]] = toIntIfInt(s[1])
                }
                return acc
            }

            exports.parseFmtpConfig = function (str) {
                return str.split(/\;\s?/).reduce(fmtpReducer, {})
            }

            exports.parsePayloads = function (str) {
                return str.split(' ').map(Number)
            }

            exports.parseRemoteCandidates = function (str) {
                var candidates = []
                var parts = str.split(' ').map(toIntIfInt)
                for (var i = 0; i < parts.length; i += 3) {
                    candidates.push({
                        component: parts[i],
                        ip: parts[i + 1],
                        port: parts[i + 2]
                    })
                }
                return candidates
            }

        }, {'./grammar': 13}],
        16: [function (require, module, exports) {
            var grammar = require('./grammar')

// customized util.format - discards excess arguments and can void middle ones
            var formatRegExp = /%[sdv%]/g
            var format = function (formatStr) {
                var i = 1
                var args = arguments
                var len = args.length
                return formatStr.replace(formatRegExp, function (x) {
                    if (i >= len) {
                        return x // missing argument
                    }
                    var arg = args[i]
                    i += 1
                    switch (x) {
                        case '%%':
                            return '%'
                        case '%s':
                            return String(arg)
                        case '%d':
                            return Number(arg)
                        case '%v':
                            return ''
                    }
                })
                // NB: we discard excess arguments - they are typically undefined from makeLine
            }

            var makeLine = function (type, obj, location) {
                var str = obj.format instanceof Function ?
                    (obj.format(obj.push ? location : location[obj.name])) :
                    obj.format

                var args = [type + '=' + str]
                if (obj.names) {
                    for (var i = 0; i < obj.names.length; i += 1) {
                        var n = obj.names[i]
                        if (obj.name) {
                            args.push(location[obj.name][n])
                        } else { // for mLine and push attributes
                            args.push(location[obj.names[i]])
                        }
                    }
                } else {
                    args.push(location[obj.name])
                }
                return format.apply(null, args)
            }

// RFC specified order
// TODO: extend this with all the rest
            var defaultOuterOrder = [
                'v', 'o', 's', 'i',
                'u', 'e', 'p', 'c',
                'b', 't', 'r', 'z', 'a'
            ]
            var defaultInnerOrder = ['i', 'c', 'b', 'a']


            module.exports = function (session, opts) {
                opts = opts || {}
                // ensure certain properties exist
                if (session.version == null) {
                    session.version = 0 // "v=0" must be there (only defined version atm)
                }
                if (session.name == null) {
                    session.name = ' ' // "s= " must be there if no meaningful name set
                }
                session.media.forEach(function (mLine) {
                    if (mLine.payloads == null) {
                        mLine.payloads = ''
                    }
                })

                var outerOrder = opts.outerOrder || defaultOuterOrder
                var innerOrder = opts.innerOrder || defaultInnerOrder
                var sdp = []

                // loop through outerOrder for matching properties on session
                outerOrder.forEach(function (type) {
                    grammar[type].forEach(function (obj) {
                        if (obj.name in session && session[obj.name] != null) {
                            sdp.push(makeLine(type, obj, session))
                        } else if (obj.push in session && session[obj.push] != null) {
                            session[obj.push].forEach(function (el) {
                                sdp.push(makeLine(type, obj, el))
                            })
                        }
                    })
                })

                // then for each media line, follow the innerOrder
                session.media.forEach(function (mLine) {
                    sdp.push(makeLine('m', grammar.m[0], mLine))

                    innerOrder.forEach(function (type) {
                        grammar[type].forEach(function (obj) {
                            if (obj.name in mLine && mLine[obj.name] != null) {
                                sdp.push(makeLine(type, obj, mLine))
                            } else if (obj.push in mLine && mLine[obj.push] != null) {
                                mLine[obj.push].forEach(function (el) {
                                    sdp.push(makeLine(type, obj, el))
                                })
                            }
                        })
                    })
                })

                return sdp.join('\r\n') + '\r\n'
            }

        }, {'./grammar': 13}],
        17: [function (require, module, exports) {
            /* Copyright @ 2015 Atlassian Pty Ltd
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

            module.exports = function arrayEquals(array) {
                // if the other array is a falsy value, return
                if (!array)
                    return false

                // compare lengths - can save a lot of time
                if (this.length != array.length)
                    return false

                for (var i = 0, l = this.length; i < l; i++) {
                    // Check if we have nested arrays
                    if (this[i] instanceof Array && array[i] instanceof Array) {
                        // recurse into the nested arrays
                        if (!arrayEquals.apply(this[i], [array[i]]))
                            return false
                    } else if (this[i] != array[i]) {
                        // Warning - two different object instances will never be equal:
                        // {x:20} != {x:20}
                        return false
                    }
                }
                return true
            }


        }, {}],
        18: [function (require, module, exports) {
            /* Copyright @ 2015 Atlassian Pty Ltd
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

            exports.Interop = require('./interop')

        }, {'./interop': 19}],
        19: [function (require, module, exports) {
            /* Copyright @ 2015 Atlassian Pty Ltd
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

            /* global RTCSessionDescription */
            /* global RTCIceCandidate */
            /* jshint -W097 */
            'use strict'

            var transform = require('./transform')
            var arrayEquals = require('./array-equals')

            function Interop() {

                /**
                 * This map holds the most recent Unified Plan offer/answer SDP that was
                 * converted to Plan B, with the SDP type ('offer' or 'answer') as keys and
                 * the SDP string as values.
                 *
                 * @type {{}}
                 */
                this.cache = {
                    mlB2UMap: {},
                    mlU2BMap: {}
                }
            }

            module.exports = Interop

            /**
             * Changes the candidate args to match with the related Unified Plan
             */
            Interop.prototype.candidateToUnifiedPlan = function (candidate) {
                var cand = new RTCIceCandidate(candidate)

                cand.sdpMLineIndex = this.cache.mlB2UMap[cand.sdpMLineIndex]
                /* TODO: change sdpMid to (audio|video)-SSRC */

                return cand
            }

            /**
             * Changes the candidate args to match with the related Plan B
             */
            Interop.prototype.candidateToPlanB = function (candidate) {
                var cand = new RTCIceCandidate(candidate)

                if (cand.sdpMid.indexOf('audio') === 0) {
                    cand.sdpMid = 'audio'
                } else if (cand.sdpMid.indexOf('video') === 0) {
                    cand.sdpMid = 'video'
                } else {
                    throw new Error('candidate with ' + cand.sdpMid + ' not allowed')
                }

                cand.sdpMLineIndex = this.cache.mlU2BMap[cand.sdpMLineIndex]

                return cand
            }

            /**
             * Returns the index of the first m-line with the given media type and with a
             * direction which allows sending, in the last Unified Plan description with
             * type "answer" converted to Plan B. Returns {null} if there is no saved
             * answer, or if none of its m-lines with the given type allow sending.
             * @param type the media type ("audio" or "video").
             * @returns {*}
             */
            Interop.prototype.getFirstSendingIndexFromAnswer = function (type) {
                if (!this.cache.answer) {
                    return null
                }

                var session = transform.parse(this.cache.answer)
                if (session && session.media && Array.isArray(session.media)) {
                    for (var i = 0; i < session.media.length; i++) {
                        if (session.media[i].type == type &&
                            (!session.media[i].direction /* default to sendrecv */ ||
                                session.media[i].direction === 'sendrecv' ||
                                session.media[i].direction === 'sendonly')) {
                            return i
                        }
                    }
                }

                return null
            }

            /**
             * This method transforms a Unified Plan SDP to an equivalent Plan B SDP. A
             * PeerConnection wrapper transforms the SDP to Plan B before passing it to the
             * application.
             *
             * @param desc
             * @returns {*}
             */
            Interop.prototype.toPlanB = function (desc) {
                var self = this
                //#region Preliminary input validation.

                if (typeof desc !== 'object' || desc === null ||
                    typeof desc.sdp !== 'string') {
                    console.warn('An empty description was passed as an argument.')
                    return desc
                }

                // Objectify the SDP for easier manipulation.
                var session = transform.parse(desc.sdp)

                // If the SDP contains no media, there's nothing to transform.
                if (typeof session.media === 'undefined' ||
                    !Array.isArray(session.media) || session.media.length === 0) {
                    console.warn('The description has no media.')
                    return desc
                }

                // Try some heuristics to "make sure" this is a Unified Plan SDP. Plan B
                // SDP has a video, an audio and a data "channel" at most.
                if (session.media.length <= 3 && session.media.every(function (m) {
                    return ['video', 'audio', 'data'].indexOf(m.mid) !== -1
                })) {
                    console.warn('This description does not look like Unified Plan.')
                    return desc
                }

                //#endregion

                // HACK https://bugzilla.mozilla.org/show_bug.cgi?id=1113443
                var sdp = desc.sdp
                var rewrite = false
                for (var i = 0; i < session.media.length; i++) {
                    var uLine = session.media[i]
                    uLine.rtp.forEach(function (rtp) {
                        if (rtp.codec === 'NULL') {
                            rewrite = true
                            var offer = transform.parse(self.cache.offer)
                            rtp.codec = offer.media[i].rtp[0].codec
                        }
                    })
                }
                if (rewrite) {
                    sdp = transform.write(session)
                }

                // Unified Plan SDP is our "precious". Cache it for later use in the Plan B
                // -> Unified Plan transformation.
                this.cache[desc.type] = sdp

                //#region Convert from Unified Plan to Plan B.

                // We rebuild the session.media array.
                var media = session.media
                session.media = []

                // Associative array that maps channel types to channel objects for fast
                // access to channel objects by their type, e.g. type2bl['audio']->channel
                // obj.
                var type2bl = {}

                // Used to build the group:BUNDLE value after the channels construction
                // loop.
                var types = []

                media.forEach(function (uLine) {
                    // rtcp-mux is required in the Plan B SDP.
                    if ((typeof uLine.rtcpMux !== 'string' ||
                        uLine.rtcpMux !== 'rtcp-mux') &&
                        uLine.direction !== 'inactive') {
                        throw new Error('Cannot convert to Plan B because m-lines ' +
                            'without the rtcp-mux attribute were found.')
                    }

                    // If we don't have a channel for this uLine.type OR the selected is
                    // inactive, then select this uLine as the channel basis.
                    if (typeof type2bl[uLine.type] === 'undefined' ||
                        type2bl[uLine.type].direction === 'inactive') {
                        type2bl[uLine.type] = uLine
                    }

                    if (uLine.protocol != type2bl[uLine.type].protocol) {
                        throw new Error('Cannot convert to Plan B because m-lines ' +
                            'have different protocols and this library does not have ' +
                            'support for that')
                    }

                    if (uLine.payloads != type2bl[uLine.type].payloads) {
                        throw new Error('Cannot convert to Plan B because m-lines ' +
                            'have different payloads and this library does not have ' +
                            'support for that')
                    }

                })

                // Implode the Unified Plan m-lines/tracks into Plan B channels.
                media.forEach(function (uLine) {
                    if (uLine.type === 'application') {
                        session.media.push(uLine)
                        types.push(uLine.mid)
                        return
                    }

                    // Add sources to the channel and handle a=msid.
                    if (typeof uLine.sources === 'object') {
                        Object.keys(uLine.sources).forEach(function (ssrc) {
                            if (typeof type2bl[uLine.type].sources !== 'object')
                                type2bl[uLine.type].sources = {}

                            // Assign the sources to the channel.
                            type2bl[uLine.type].sources[ssrc] =
                                uLine.sources[ssrc]

                            if (typeof uLine.msid !== 'undefined') {
                                // In Plan B the msid is an SSRC attribute. Also, we don't
                                // care about the obsolete label and mslabel attributes.
                                //
                                // Note that it is not guaranteed that the uLine will
                                // have an msid. recvonly channels in particular don't have
                                // one.
                                type2bl[uLine.type].sources[ssrc].msid =
                                    uLine.msid
                            }
                            // NOTE ssrcs in ssrc groups will share msids, as
                            // draft-uberti-rtcweb-plan-00 mandates.
                        })
                    }

                    // Add ssrc groups to the channel.
                    if (typeof uLine.ssrcGroups !== 'undefined' &&
                        Array.isArray(uLine.ssrcGroups)) {

                        // Create the ssrcGroups array, if it's not defined.
                        if (typeof type2bl[uLine.type].ssrcGroups === 'undefined' ||
                            !Array.isArray(type2bl[uLine.type].ssrcGroups)) {
                            type2bl[uLine.type].ssrcGroups = []
                        }

                        type2bl[uLine.type].ssrcGroups =
                            type2bl[uLine.type].ssrcGroups.concat(
                                uLine.ssrcGroups)
                    }

                    if (type2bl[uLine.type] === uLine) {
                        // Plan B mids are in ['audio', 'video', 'data']
                        uLine.mid = uLine.type

                        // Plan B doesn't support/need the bundle-only attribute.
                        delete uLine.bundleOnly

                        // In Plan B the msid is an SSRC attribute.
                        delete uLine.msid

                        if (uLine.type == media[0].type) {
                            types.unshift(uLine.type)
                            // Add the channel to the new media array.
                            session.media.unshift(uLine)
                        } else {
                            types.push(uLine.type)
                            // Add the channel to the new media array.
                            session.media.push(uLine)
                        }
                    }
                })

                if (typeof session.groups !== 'undefined') {
                    // We regenerate the BUNDLE group with the new mids.
                    session.groups.some(function (group) {
                        if (group.type === 'BUNDLE') {
                            group.mids = types.join(' ')
                            return true
                        }
                    })
                }

                // msid semantic
                session.msidSemantic = {
                    semantic: 'WMS',
                    token: '*'
                }

                var resStr = transform.write(session)

                return new RTCSessionDescription({
                    type: desc.type,
                    sdp: resStr
                })

                //#endregion
            }

            /* follow rules defined in RFC4145 */
            function addSetupAttr(uLine) {
                if (typeof uLine.setup === 'undefined') {
                    return
                }

                if (uLine.setup === 'active') {
                    uLine.setup = 'passive'
                } else if (uLine.setup === 'passive') {
                    uLine.setup = 'active'
                }
            }

            /**
             * This method transforms a Plan B SDP to an equivalent Unified Plan SDP. A
             * PeerConnection wrapper transforms the SDP to Unified Plan before passing it
             * to FF.
             *
             * @param desc
             * @returns {*}
             */
            Interop.prototype.toUnifiedPlan = function (desc) {
                var self = this
                //#region Preliminary input validation.

                if (typeof desc !== 'object' || desc === null ||
                    typeof desc.sdp !== 'string') {
                    console.warn('An empty description was passed as an argument.')
                    return desc
                }

                var session = transform.parse(desc.sdp)

                // If the SDP contains no media, there's nothing to transform.
                if (typeof session.media === 'undefined' ||
                    !Array.isArray(session.media) || session.media.length === 0) {
                    console.warn('The description has no media.')
                    return desc
                }

                // Try some heuristics to "make sure" this is a Plan B SDP. Plan B SDP has
                // a video, an audio and a data "channel" at most.
                if (session.media.length > 3 || !session.media.every(function (m) {
                    return ['video', 'audio', 'data'].indexOf(m.mid) !== -1
                })) {
                    console.warn('This description does not look like Plan B.')
                    return desc
                }

                // Make sure this Plan B SDP can be converted to a Unified Plan SDP.
                var mids = []
                session.media.forEach(function (m) {
                    mids.push(m.mid)
                })

                var hasBundle = false
                if (typeof session.groups !== 'undefined' &&
                    Array.isArray(session.groups)) {
                    hasBundle = session.groups.every(function (g) {
                        return g.type !== 'BUNDLE' ||
                            arrayEquals.apply(g.mids.sort(), [mids.sort()])
                    })
                }

                if (!hasBundle) {
                    var mustBeBundle = false

                    session.media.forEach(function (m) {
                        if (m.direction !== 'inactive') {
                            mustBeBundle = true
                        }
                    })

                    if (mustBeBundle) {
                        throw new Error('Cannot convert to Unified Plan because m-lines that' +
                            ' are not bundled were found.')
                    }
                }

                //#endregion


                //#region Convert from Plan B to Unified Plan.

                // Unfortunately, a Plan B offer/answer doesn't have enough information to
                // rebuild an equivalent Unified Plan offer/answer.
                //
                // For example, if this is a local answer (in Unified Plan style) that we
                // convert to Plan B prior to handing it over to the application (the
                // PeerConnection wrapper called us, for instance, after a successful
                // createAnswer), we want to remember the m-line at which we've seen the
                // (local) SSRC. That's because when the application wants to do call the
                // SLD method, forcing us to do the inverse transformation (from Plan B to
                // Unified Plan), we need to know to which m-line to assign the (local)
                // SSRC. We also need to know all the other m-lines that the original
                // answer had and include them in the transformed answer as well.
                //
                // Another example is if this is a remote offer that we convert to Plan B
                // prior to giving it to the application, we want to remember the mid at
                // which we've seen the (remote) SSRC.
                //
                // In the iteration that follows, we use the cached Unified Plan (if it
                // exists) to assign mids to ssrcs.

                var type
                if (desc.type === 'answer') {
                    type = 'offer'
                } else if (desc.type === 'offer') {
                    type = 'answer'
                } else {
                    throw new Error('Type \'' + desc.type + '\' not supported.')
                }

                var cached
                if (typeof this.cache[type] !== 'undefined') {
                    cached = transform.parse(this.cache[type])
                }

                var recvonlySsrcs = {
                    audio: {},
                    video: {}
                }

                // A helper map that sends mids to m-line objects. We use it later to
                // rebuild the Unified Plan style session.media array.
                var mid2ul = {}
                var bIdx = 0
                var uIdx = 0

                var sources2ul = {}

                var candidates
                var iceUfrag
                var icePwd
                var fingerprint
                var payloads = {}
                var rtcpFb = {}
                var rtp = {}

                session.media.forEach(function (bLine) {
                    if ((typeof bLine.rtcpMux !== 'string' ||
                        bLine.rtcpMux !== 'rtcp-mux') &&
                        bLine.direction !== 'inactive') {
                        throw new Error('Cannot convert to Unified Plan because m-lines ' +
                            'without the rtcp-mux attribute were found.')
                    }

                    if (bLine.type === 'application') {
                        mid2ul[bLine.mid] = bLine
                        return
                    }

                    // With rtcp-mux and bundle all the channels should have the same ICE
                    // stuff.
                    var sources = bLine.sources
                    var ssrcGroups = bLine.ssrcGroups
                    var port = bLine.port

                    /* Chrome adds different candidates even using bundle, so we concat the candidates list */
                    if (typeof bLine.candidates != 'undefined') {
                        if (typeof candidates != 'undefined') {
                            candidates = candidates.concat(bLine.candidates)
                        } else {
                            candidates = bLine.candidates
                        }
                    }

                    if ((typeof iceUfrag != 'undefined') && (typeof bLine.iceUfrag != 'undefined') && (iceUfrag != bLine.iceUfrag)) {
                        throw new Error('Only BUNDLE supported, iceUfrag must be the same for all m-lines.\n' +
                            '\tLast iceUfrag: ' + iceUfrag + '\n' +
                            '\tNew iceUfrag: ' + bLine.iceUfrag
                        )
                    }

                    if (typeof bLine.iceUfrag != 'undefined') {
                        iceUfrag = bLine.iceUfrag
                    }

                    if ((typeof icePwd != 'undefined') && (typeof bLine.icePwd != 'undefined') && (icePwd != bLine.icePwd)) {
                        throw new Error('Only BUNDLE supported, icePwd must be the same for all m-lines.\n' +
                            '\tLast icePwd: ' + icePwd + '\n' +
                            '\tNew icePwd: ' + bLine.icePwd
                        )
                    }

                    if (typeof bLine.icePwd != 'undefined') {
                        icePwd = bLine.icePwd
                    }

                    if ((typeof fingerprint != 'undefined') && (typeof bLine.fingerprint != 'undefined') &&
                        (fingerprint.type != bLine.fingerprint.type || fingerprint.hash != bLine.fingerprint.hash)) {
                        throw new Error('Only BUNDLE supported, fingerprint must be the same for all m-lines.\n' +
                            '\tLast fingerprint: ' + JSON.stringify(fingerprint) + '\n' +
                            '\tNew fingerprint: ' + JSON.stringify(bLine.fingerprint)
                        )
                    }

                    if (typeof bLine.fingerprint != 'undefined') {
                        fingerprint = bLine.fingerprint
                    }

                    payloads[bLine.type] = bLine.payloads
                    rtcpFb[bLine.type] = bLine.rtcpFb
                    rtp[bLine.type] = bLine.rtp

                    // inverted ssrc group map
                    var ssrc2group = {}
                    if (typeof ssrcGroups !== 'undefined' && Array.isArray(ssrcGroups)) {
                        ssrcGroups.forEach(function (ssrcGroup) {
                            // XXX This might brake if an SSRC is in more than one group
                            // for some reason.
                            if (typeof ssrcGroup.ssrcs !== 'undefined' &&
                                Array.isArray(ssrcGroup.ssrcs)) {
                                ssrcGroup.ssrcs.forEach(function (ssrc) {
                                    if (typeof ssrc2group[ssrc] === 'undefined') {
                                        ssrc2group[ssrc] = []
                                    }

                                    ssrc2group[ssrc].push(ssrcGroup)
                                })
                            }
                        })
                    }

                    // ssrc to m-line index.
                    var ssrc2ml = {}

                    if (typeof sources === 'object') {

                        // We'll use the "bLine" object as a prototype for each new "mLine"
                        // that we create, but first we need to clean it up a bit.
                        delete bLine.sources
                        delete bLine.ssrcGroups
                        delete bLine.candidates
                        delete bLine.iceUfrag
                        delete bLine.icePwd
                        delete bLine.fingerprint
                        delete bLine.port
                        delete bLine.mid

                        // Explode the Plan B channel sources with one m-line per source.
                        Object.keys(sources).forEach(function (ssrc) {

                            // The (unified) m-line for this SSRC. We either create it from
                            // scratch or, if it's a grouped SSRC, we re-use a related
                            // mline. In other words, if the source is grouped with another
                            // source, put the two together in the same m-line.
                            var uLine

                            // We assume here that we are the answerer in the O/A, so any
                            // offers which we translate come from the remote side, while
                            // answers are local. So the check below is to make that we
                            // handle receive-only SSRCs in a special way only if they come
                            // from the remote side.
                            if (desc.type === 'offer') {
                                // We want to detect SSRCs which are used by a remote peer
                                // in an m-line with direction=recvonly (i.e. they are
                                // being used for RTCP only).
                                // This information would have gotten lost if the remote
                                // peer used Unified Plan and their local description was
                                // translated to Plan B. So we use the lack of an MSID
                                // attribute to deduce a "receive only" SSRC.
                                if (!sources[ssrc].msid) {
                                    recvonlySsrcs[bLine.type][ssrc] = sources[ssrc]
                                    // Receive-only SSRCs must not create new m-lines. We
                                    // will assign them to an existing m-line later.
                                    return
                                }
                            }

                            if (typeof ssrc2group[ssrc] !== 'undefined' &&
                                Array.isArray(ssrc2group[ssrc])) {
                                ssrc2group[ssrc].some(function (ssrcGroup) {
                                    // ssrcGroup.ssrcs *is* an Array, no need to check
                                    // again here.
                                    return ssrcGroup.ssrcs.some(function (related) {
                                        if (typeof ssrc2ml[related] === 'object') {
                                            uLine = ssrc2ml[related]
                                            return true
                                        }
                                    })
                                })
                            }

                            if (typeof uLine === 'object') {
                                // the m-line already exists. Just add the source.
                                uLine.sources[ssrc] = sources[ssrc]
                                delete sources[ssrc].msid
                            } else {
                                // Use the "bLine" as a prototype for the "uLine".
                                uLine = Object.create(bLine)
                                ssrc2ml[ssrc] = uLine

                                if (typeof sources[ssrc].msid !== 'undefined') {
                                    // Assign the msid of the source to the m-line. Note
                                    // that it is not guaranteed that the source will have
                                    // msid. In particular "recvonly" sources don't have an
                                    // msid. Note that "recvonly" is a term only defined
                                    // for m-lines.
                                    uLine.msid = sources[ssrc].msid
                                    delete sources[ssrc].msid
                                }

                                // We assign one SSRC per media line.
                                uLine.sources = {}
                                uLine.sources[ssrc] = sources[ssrc]
                                uLine.ssrcGroups = ssrc2group[ssrc]

                                // Use the cached Unified Plan SDP (if it exists) to assign
                                // SSRCs to mids.
                                if (typeof cached !== 'undefined' &&
                                    typeof cached.media !== 'undefined' &&
                                    Array.isArray(cached.media)) {

                                    cached.media.forEach(function (m) {
                                        if (typeof m.sources === 'object') {
                                            Object.keys(m.sources).forEach(function (s) {
                                                if (s === ssrc) {
                                                    uLine.mid = m.mid
                                                }
                                            })
                                        }
                                    })
                                }

                                if (typeof uLine.mid === 'undefined') {

                                    // If this is an SSRC that we see for the first time
                                    // assign it a new mid. This is typically the case when
                                    // this method is called to transform a remote
                                    // description for the first time or when there is a
                                    // new SSRC in the remote description because a new
                                    // peer has joined the conference. Local SSRCs should
                                    // have already been added to the map in the toPlanB
                                    // method.
                                    //
                                    // Because FF generates answers in Unified Plan style,
                                    // we MUST already have a cached answer with all the
                                    // local SSRCs mapped to some m-line/mid.

                                    uLine.mid = [bLine.type, '-', ssrc].join('')
                                }

                                // Include the candidates in the 1st media line.
                                uLine.candidates = candidates
                                uLine.iceUfrag = iceUfrag
                                uLine.icePwd = icePwd
                                uLine.fingerprint = fingerprint
                                uLine.port = port

                                mid2ul[uLine.mid] = uLine
                                sources2ul[uIdx] = uLine.sources

                                self.cache.mlU2BMap[uIdx] = bIdx
                                if (typeof self.cache.mlB2UMap[bIdx] === 'undefined') {
                                    self.cache.mlB2UMap[bIdx] = uIdx
                                }
                                uIdx++
                            }
                        })
                    } else {
                        var uLine = bLine

                        uLine.candidates = candidates
                        uLine.iceUfrag = iceUfrag
                        uLine.icePwd = icePwd
                        uLine.fingerprint = fingerprint
                        uLine.port = port

                        mid2ul[uLine.mid] = uLine

                        self.cache.mlU2BMap[uIdx] = bIdx
                        if (typeof self.cache.mlB2UMap[bIdx] === 'undefined') {
                            self.cache.mlB2UMap[bIdx] = uIdx
                        }
                    }

                    bIdx++
                })

                // Rebuild the media array in the right order and add the missing mLines
                // (missing from the Plan B SDP).
                session.media = []
                mids = [] // reuse

                if (desc.type === 'answer') {

                    // The media lines in the answer must match the media lines in the
                    // offer. The order is important too. Here we assume that Firefox is
                    // the answerer, so we merely have to use the reconstructed (unified)
                    // answer to update the cached (unified) answer accordingly.
                    //
                    // In the general case, one would have to use the cached (unified)
                    // offer to find the m-lines that are missing from the reconstructed
                    // answer, potentially grabbing them from the cached (unified) answer.
                    // One has to be careful with this approach because inactive m-lines do
                    // not always have an mid, making it tricky (impossible?) to find where
                    // exactly and which m-lines are missing from the reconstructed answer.

                    for (var i = 0; i < cached.media.length; i++) {
                        var uLine = cached.media[i]

                        delete uLine.msid
                        delete uLine.sources
                        delete uLine.ssrcGroups

                        if (typeof sources2ul[i] === 'undefined') {
                            if (!uLine.direction
                                || uLine.direction === 'sendrecv')
                                uLine.direction = 'recvonly'
                            else if (uLine.direction === 'sendonly')
                                uLine.direction = 'inactive'
                        } else {
                            if (!uLine.direction
                                || uLine.direction === 'sendrecv')
                                uLine.direction = 'sendrecv'
                            else if (uLine.direction === 'recvonly')
                                uLine.direction = 'sendonly'
                        }

                        uLine.sources = sources2ul[i]
                        uLine.candidates = candidates
                        uLine.iceUfrag = iceUfrag
                        uLine.icePwd = icePwd
                        uLine.fingerprint = fingerprint

                        uLine.rtp = rtp[uLine.type]
                        uLine.payloads = payloads[uLine.type]
                        uLine.rtcpFb = rtcpFb[uLine.type]

                        session.media.push(uLine)

                        if (typeof uLine.mid === 'string') {
                            // inactive lines don't/may not have an mid.
                            mids.push(uLine.mid)
                        }
                    }
                } else {

                    // SDP offer/answer (and the JSEP spec) forbids removing an m-section
                    // under any circumstances. If we are no longer interested in sending a
                    // track, we just remove the msid and ssrc attributes and set it to
                    // either a=recvonly (as the reofferer, we must use recvonly if the
                    // other side was previously sending on the m-section, but we can also
                    // leave the possibility open if it wasn't previously in use), or
                    // a=inactive.

                    if (typeof cached !== 'undefined' &&
                        typeof cached.media !== 'undefined' &&
                        Array.isArray(cached.media)) {
                        cached.media.forEach(function (uLine) {
                            mids.push(uLine.mid)
                            if (typeof mid2ul[uLine.mid] !== 'undefined') {
                                session.media.push(mid2ul[uLine.mid])
                            } else {
                                delete uLine.msid
                                delete uLine.sources
                                delete uLine.ssrcGroups

                                if (!uLine.direction
                                    || uLine.direction === 'sendrecv') {
                                    uLine.direction = 'sendonly'
                                }
                                if (!uLine.direction
                                    || uLine.direction === 'recvonly') {
                                    uLine.direction = 'inactive'
                                }

                                addSetupAttr(uLine)
                                session.media.push(uLine)
                            }
                        })
                    }

                    // Add all the remaining (new) m-lines of the transformed SDP.
                    Object.keys(mid2ul).forEach(function (mid) {
                        if (mids.indexOf(mid) === -1) {
                            mids.push(mid)
                            if (mid2ul[mid].direction === 'recvonly') {
                                // This is a remote recvonly channel. Add its SSRC to the
                                // appropriate sendrecv or sendonly channel.
                                // TODO(gp) what if we don't have sendrecv/sendonly
                                // channel?

                                var done = false

                                session.media.some(function (uLine) {
                                    if ((uLine.direction === 'sendrecv' ||
                                        uLine.direction === 'sendonly') &&
                                        uLine.type === mid2ul[mid].type) {
                                        // mid2ul[mid] shouldn't have any ssrc-groups
                                        Object.keys(mid2ul[mid].sources).forEach(
                                            function (ssrc) {
                                                uLine.sources[ssrc] =
                                                    mid2ul[mid].sources[ssrc]
                                            })

                                        done = true
                                        return true
                                    }
                                })

                                if (!done) {
                                    session.media.push(mid2ul[mid])
                                }
                            } else {
                                session.media.push(mid2ul[mid])
                            }
                        }
                    })
                }

                // After we have constructed the Plan Unified m-lines we can figure out
                // where (in which m-line) to place the 'recvonly SSRCs'.
                // Note: we assume here that we are the answerer in the O/A, so any offers
                // which we translate come from the remote side, while answers are local
                // (and so our last local description is cached as an 'answer').
                ['audio', 'video'].forEach(function (type) {
                    if (!session || !session.media || !Array.isArray(session.media))
                        return

                    var idx = null
                    if (Object.keys(recvonlySsrcs[type]).length > 0) {
                        idx = self.getFirstSendingIndexFromAnswer(type)
                        if (idx === null) {
                            // If this is the first offer we receive, we don't have a
                            // cached answer. Assume that we will be sending media using
                            // the first m-line for each media type.

                            for (var i = 0; i < session.media.length; i++) {
                                if (session.media[i].type === type) {
                                    idx = i
                                    break
                                }
                            }
                        }
                    }

                    if (idx && session.media.length > idx) {
                        var mLine = session.media[idx]
                        Object.keys(recvonlySsrcs[type]).forEach(function (ssrc) {
                            if (mLine.sources && mLine.sources[ssrc]) {
                                console.warn('Replacing an existing SSRC.')
                            }
                            if (!mLine.sources) {
                                mLine.sources = {}
                            }

                            mLine.sources[ssrc] = recvonlySsrcs[type][ssrc]
                        })
                    }
                })

                if (typeof session.groups !== 'undefined') {
                    // We regenerate the BUNDLE group (since we regenerated the mids)
                    session.groups.some(function (group) {
                        if (group.type === 'BUNDLE') {
                            group.mids = mids.join(' ')
                            return true
                        }
                    })
                }

                // msid semantic
                session.msidSemantic = {
                    semantic: 'WMS',
                    token: '*'
                }

                var resStr = transform.write(session)

                // Cache the transformed SDP (Unified Plan) for later re-use in this
                // function.
                this.cache[desc.type] = resStr

                return new RTCSessionDescription({
                    type: desc.type,
                    sdp: resStr
                })

                //#endregion
            }

        }, {'./array-equals': 17, './transform': 20}],
        20: [function (require, module, exports) {
            /* Copyright @ 2015 Atlassian Pty Ltd
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

            var transform = require('sdp-transform')

            exports.write = function (session, opts) {

                if (typeof session !== 'undefined' &&
                    typeof session.media !== 'undefined' &&
                    Array.isArray(session.media)) {

                    session.media.forEach(function (mLine) {
                        // expand sources to ssrcs
                        if (typeof mLine.sources !== 'undefined' &&
                            Object.keys(mLine.sources).length !== 0) {
                            mLine.ssrcs = []
                            Object.keys(mLine.sources).forEach(function (ssrc) {
                                var source = mLine.sources[ssrc]
                                Object.keys(source).forEach(function (attribute) {
                                    mLine.ssrcs.push({
                                        id: ssrc,
                                        attribute: attribute,
                                        value: source[attribute]
                                    })
                                })
                            })
                            delete mLine.sources
                        }

                        // join ssrcs in ssrc groups
                        if (typeof mLine.ssrcGroups !== 'undefined' &&
                            Array.isArray(mLine.ssrcGroups)) {
                            mLine.ssrcGroups.forEach(function (ssrcGroup) {
                                if (typeof ssrcGroup.ssrcs !== 'undefined' &&
                                    Array.isArray(ssrcGroup.ssrcs)) {
                                    ssrcGroup.ssrcs = ssrcGroup.ssrcs.join(' ')
                                }
                            })
                        }
                    })
                }

                // join group mids
                if (typeof session !== 'undefined' &&
                    typeof session.groups !== 'undefined' && Array.isArray(session.groups)) {

                    session.groups.forEach(function (g) {
                        if (typeof g.mids !== 'undefined' && Array.isArray(g.mids)) {
                            g.mids = g.mids.join(' ')
                        }
                    })
                }

                return transform.write(session, opts)
            }

            exports.parse = function (sdp) {
                var session = transform.parse(sdp)

                if (typeof session !== 'undefined' && typeof session.media !== 'undefined' &&
                    Array.isArray(session.media)) {

                    session.media.forEach(function (mLine) {
                        // group sources attributes by ssrc
                        if (typeof mLine.ssrcs !== 'undefined' && Array.isArray(mLine.ssrcs)) {
                            mLine.sources = {}
                            mLine.ssrcs.forEach(function (ssrc) {
                                if (!mLine.sources[ssrc.id])
                                    mLine.sources[ssrc.id] = {}
                                mLine.sources[ssrc.id][ssrc.attribute] = ssrc.value
                            })

                            delete mLine.ssrcs
                        }

                        // split ssrcs in ssrc groups
                        if (typeof mLine.ssrcGroups !== 'undefined' &&
                            Array.isArray(mLine.ssrcGroups)) {
                            mLine.ssrcGroups.forEach(function (ssrcGroup) {
                                if (typeof ssrcGroup.ssrcs === 'string') {
                                    ssrcGroup.ssrcs = ssrcGroup.ssrcs.split(' ')
                                }
                            })
                        }
                    })
                }
                // split group mids
                if (typeof session !== 'undefined' &&
                    typeof session.groups !== 'undefined' && Array.isArray(session.groups)) {

                    session.groups.forEach(function (g) {
                        if (typeof g.mids === 'string') {
                            g.mids = g.mids.split(' ')
                        }
                    })
                }

                return session
            }


        }, {'sdp-transform': 14}],
        21: [function (require, module, exports) {
/////////////////////////////////////////////////////////////////////////////////
            /* UAParser.js v0.7.32
   Copyright © 2012-2021 Faisal Salman <f@faisalman.com>
   MIT License *//*
   Detect Browser, Engine, OS, CPU, and Device type/model from User-Agent data.
   Supports browser & node.js environment.
   Demo   : https://faisalman.github.io/ua-parser-js
   Source : https://github.com/faisalman/ua-parser-js */
/////////////////////////////////////////////////////////////////////////////////

            (function (window, undefined) {

                'use strict'

                //////////////
                // Constants
                /////////////


                var LIBVERSION = '0.7.32',
                    EMPTY = '',
                    UNKNOWN = '?',
                    FUNC_TYPE = 'function',
                    UNDEF_TYPE = 'undefined',
                    OBJ_TYPE = 'object',
                    STR_TYPE = 'string',
                    MAJOR = 'major',
                    MODEL = 'model',
                    NAME = 'name',
                    TYPE = 'type',
                    VENDOR = 'vendor',
                    VERSION = 'version',
                    ARCHITECTURE = 'architecture',
                    CONSOLE = 'console',
                    MOBILE = 'mobile',
                    TABLET = 'tablet',
                    SMARTTV = 'smarttv',
                    WEARABLE = 'wearable',
                    EMBEDDED = 'embedded',
                    UA_MAX_LENGTH = 350

                var AMAZON = 'Amazon',
                    APPLE = 'Apple',
                    ASUS = 'ASUS',
                    BLACKBERRY = 'BlackBerry',
                    BROWSER = 'Browser',
                    CHROME = 'Chrome',
                    EDGE = 'Edge',
                    FIREFOX = 'Firefox',
                    GOOGLE = 'Google',
                    HUAWEI = 'Huawei',
                    LG = 'LG',
                    MICROSOFT = 'Microsoft',
                    MOTOROLA = 'Motorola',
                    OPERA = 'Opera',
                    SAMSUNG = 'Samsung',
                    SHARP = 'Sharp',
                    SONY = 'Sony',
                    XIAOMI = 'Xiaomi',
                    ZEBRA = 'Zebra',
                    FACEBOOK = 'Facebook'

                ///////////
                // Helper
                //////////

                var extend = function (regexes, extensions) {
                        var mergedRegexes = {}
                        for (var i in regexes) {
                            if (extensions[i] && extensions[i].length % 2 === 0) {
                                mergedRegexes[i] = extensions[i].concat(regexes[i])
                            } else {
                                mergedRegexes[i] = regexes[i]
                            }
                        }
                        return mergedRegexes
                    },
                    enumerize = function (arr) {
                        var enums = {}
                        for (var i = 0; i < arr.length; i++) {
                            enums[arr[i].toUpperCase()] = arr[i]
                        }
                        return enums
                    },
                    has = function (str1, str2) {
                        return typeof str1 === STR_TYPE ? lowerize(str2).indexOf(lowerize(str1)) !== -1 : false
                    },
                    lowerize = function (str) {
                        return str.toLowerCase()
                    },
                    majorize = function (version) {
                        return typeof (version) === STR_TYPE ? version.replace(/[^\d\.]/g, EMPTY).split('.')[0] : undefined
                    },
                    trim = function (str, len) {
                        if (typeof (str) === STR_TYPE) {
                            str = str.replace(/^\s\s*/, EMPTY).replace(/\s\s*$/, EMPTY)
                            return typeof (len) === UNDEF_TYPE ? str : str.substring(0, UA_MAX_LENGTH)
                        }
                    }

                ///////////////
                // Map helper
                //////////////

                var rgxMapper = function (ua, arrays) {

                        var i = 0, j, k, p, q, matches, match

                        // loop through all regexes maps
                        while (i < arrays.length && !matches) {

                            var regex = arrays[i],       // even sequence (0,2,4,..)
                                props = arrays[i + 1]   // odd sequence (1,3,5,..)
                            j = k = 0

                            // try matching uastring with regexes
                            while (j < regex.length && !matches) {

                                matches = regex[j++].exec(ua)

                                if (!!matches) {
                                    for (p = 0; p < props.length; p++) {
                                        match = matches[++k]
                                        q = props[p]
                                        // check if given property is actually array
                                        if (typeof q === OBJ_TYPE && q.length > 0) {
                                            if (q.length === 2) {
                                                if (typeof q[1] == FUNC_TYPE) {
                                                    // assign modified match
                                                    this[q[0]] = q[1].call(this, match)
                                                } else {
                                                    // assign given value, ignore regex match
                                                    this[q[0]] = q[1]
                                                }
                                            } else if (q.length === 3) {
                                                // check whether function or regex
                                                if (typeof q[1] === FUNC_TYPE && !(q[1].exec && q[1].test)) {
                                                    // call function (usually string mapper)
                                                    this[q[0]] = match ? q[1].call(this, match, q[2]) : undefined
                                                } else {
                                                    // sanitize match using given regex
                                                    this[q[0]] = match ? match.replace(q[1], q[2]) : undefined
                                                }
                                            } else if (q.length === 4) {
                                                this[q[0]] = match ? q[3].call(this, match.replace(q[1], q[2])) : undefined
                                            }
                                        } else {
                                            this[q] = match ? match : undefined
                                        }
                                    }
                                }
                            }
                            i += 2
                        }
                    },

                    strMapper = function (str, map) {

                        for (var i in map) {
                            // check if current value is array
                            if (typeof map[i] === OBJ_TYPE && map[i].length > 0) {
                                for (var j = 0; j < map[i].length; j++) {
                                    if (has(map[i][j], str)) {
                                        return (i === UNKNOWN) ? undefined : i
                                    }
                                }
                            } else if (has(map[i], str)) {
                                return (i === UNKNOWN) ? undefined : i
                            }
                        }
                        return str
                    }

                ///////////////
                // String map
                //////////////

                // Safari < 3.0
                var oldSafariMap = {
                        '1.0': '/8',
                        '1.2': '/1',
                        '1.3': '/3',
                        '2.0': '/412',
                        '2.0.2': '/416',
                        '2.0.3': '/417',
                        '2.0.4': '/419',
                        '?': '/'
                    },
                    windowsVersionMap = {
                        'ME': '4.90',
                        'NT 3.11': 'NT3.51',
                        'NT 4.0': 'NT4.0',
                        '2000': 'NT 5.0',
                        'XP': ['NT 5.1', 'NT 5.2'],
                        'Vista': 'NT 6.0',
                        '7': 'NT 6.1',
                        '8': 'NT 6.2',
                        '8.1': 'NT 6.3',
                        '10': ['NT 6.4', 'NT 10.0'],
                        'RT': 'ARM'
                    }

                //////////////
                // Regex map
                /////////////

                var regexes = {

                    browser: [[

                        /\b(?:crmo|crios)\/([\w\.]+)/i                                      // Chrome for Android/iOS
                    ], [VERSION, [NAME, 'Chrome']], [
                        /edg(?:e|ios|a)?\/([\w\.]+)/i                                       // Microsoft Edge
                    ], [VERSION, [NAME, 'Edge']], [

                        // Presto based
                        /(opera mini)\/([-\w\.]+)/i,                                        // Opera Mini
                        /(opera [mobiletab]{3,6})\b.+version\/([-\w\.]+)/i,                 // Opera Mobi/Tablet
                        /(opera)(?:.+version\/|[\/ ]+)([\w\.]+)/i                           // Opera
                    ], [NAME, VERSION], [
                        /opios[\/ ]+([\w\.]+)/i                                             // Opera mini on iphone >= 8.0
                    ], [VERSION, [NAME, OPERA + ' Mini']], [
                        /\bopr\/([\w\.]+)/i                                                 // Opera Webkit
                    ], [VERSION, [NAME, OPERA]], [

                        // Mixed
                        /(kindle)\/([\w\.]+)/i,                                             // Kindle
                        /(lunascape|maxthon|netfront|jasmine|blazer)[\/ ]?([\w\.]*)/i,      // Lunascape/Maxthon/Netfront/Jasmine/Blazer
                        // Trident based
                        /(avant |iemobile|slim)(?:browser)?[\/ ]?([\w\.]*)/i,               // Avant/IEMobile/SlimBrowser
                        /(ba?idubrowser)[\/ ]?([\w\.]+)/i,                                  // Baidu Browser
                        /(?:ms|\()(ie) ([\w\.]+)/i,                                         // Internet Explorer

                        // Webkit/KHTML based                                               // Flock/RockMelt/Midori/Epiphany/Silk/Skyfire/Bolt/Iron/Iridium/PhantomJS/Bowser/QupZilla/Falkon
                        /(flock|rockmelt|midori|epiphany|silk|skyfire|ovibrowser|bolt|iron|vivaldi|iridium|phantomjs|bowser|quark|qupzilla|falkon|rekonq|puffin|brave|whale|qqbrowserlite|qq|duckduckgo)\/([-\w\.]+)/i,
                        // Rekonq/Puffin/Brave/Whale/QQBrowserLite/QQ, aka ShouQ
                        /(weibo)__([\d\.]+)/i                                               // Weibo
                    ], [NAME, VERSION], [
                        /(?:\buc? ?browser|(?:juc.+)ucweb)[\/ ]?([\w\.]+)/i                 // UCBrowser
                    ], [VERSION, [NAME, 'UC' + BROWSER]], [
                        /microm.+\bqbcore\/([\w\.]+)/i,                                     // WeChat Desktop for Windows Built-in Browser
                        /\bqbcore\/([\w\.]+).+microm/i
                    ], [VERSION, [NAME, 'WeChat(Win) Desktop']], [
                        /micromessenger\/([\w\.]+)/i                                        // WeChat
                    ], [VERSION, [NAME, 'WeChat']], [
                        /konqueror\/([\w\.]+)/i                                             // Konqueror
                    ], [VERSION, [NAME, 'Konqueror']], [
                        /trident.+rv[: ]([\w\.]{1,9})\b.+like gecko/i                       // IE11
                    ], [VERSION, [NAME, 'IE']], [
                        /yabrowser\/([\w\.]+)/i                                             // Yandex
                    ], [VERSION, [NAME, 'Yandex']], [
                        /(avast|avg)\/([\w\.]+)/i                                           // Avast/AVG Secure Browser
                    ], [[NAME, /(.+)/, '$1 Secure ' + BROWSER], VERSION], [
                        /\bfocus\/([\w\.]+)/i                                               // Firefox Focus
                    ], [VERSION, [NAME, FIREFOX + ' Focus']], [
                        /\bopt\/([\w\.]+)/i                                                 // Opera Touch
                    ], [VERSION, [NAME, OPERA + ' Touch']], [
                        /coc_coc\w+\/([\w\.]+)/i                                            // Coc Coc Browser
                    ], [VERSION, [NAME, 'Coc Coc']], [
                        /dolfin\/([\w\.]+)/i                                                // Dolphin
                    ], [VERSION, [NAME, 'Dolphin']], [
                        /coast\/([\w\.]+)/i                                                 // Opera Coast
                    ], [VERSION, [NAME, OPERA + ' Coast']], [
                        /miuibrowser\/([\w\.]+)/i                                           // MIUI Browser
                    ], [VERSION, [NAME, 'MIUI ' + BROWSER]], [
                        /fxios\/([-\w\.]+)/i                                                // Firefox for iOS
                    ], [VERSION, [NAME, FIREFOX]], [
                        /\bqihu|(qi?ho?o?|360)browser/i                                     // 360
                    ], [[NAME, '360 ' + BROWSER]], [
                        /(oculus|samsung|sailfish|huawei)browser\/([\w\.]+)/i
                    ], [[NAME, /(.+)/, '$1 ' + BROWSER], VERSION], [                      // Oculus/Samsung/Sailfish/Huawei Browser
                        /(comodo_dragon)\/([\w\.]+)/i                                       // Comodo Dragon
                    ], [[NAME, /_/g, ' '], VERSION], [
                        /(electron)\/([\w\.]+) safari/i,                                    // Electron-based App
                        /(tesla)(?: qtcarbrowser|\/(20\d\d\.[-\w\.]+))/i,                   // Tesla
                        /m?(qqbrowser|baiduboxapp|2345Explorer)[\/ ]?([\w\.]+)/i            // QQBrowser/Baidu App/2345 Browser
                    ], [NAME, VERSION], [
                        /(metasr)[\/ ]?([\w\.]+)/i,                                         // SouGouBrowser
                        /(lbbrowser)/i,                                                     // LieBao Browser
                        /\[(linkedin)app\]/i                                                // LinkedIn App for iOS & Android
                    ], [NAME], [

                        // WebView
                        /((?:fban\/fbios|fb_iab\/fb4a)(?!.+fbav)|;fbav\/([\w\.]+);)/i       // Facebook App for iOS & Android
                    ], [[NAME, FACEBOOK], VERSION], [
                        /safari (line)\/([\w\.]+)/i,                                        // Line App for iOS
                        /\b(line)\/([\w\.]+)\/iab/i,                                        // Line App for Android
                        /(chromium|instagram)[\/ ]([-\w\.]+)/i                              // Chromium/Instagram
                    ], [NAME, VERSION], [
                        /\bgsa\/([\w\.]+) .*safari\//i                                      // Google Search Appliance on iOS
                    ], [VERSION, [NAME, 'GSA']], [

                        /headlesschrome(?:\/([\w\.]+)| )/i                                  // Chrome Headless
                    ], [VERSION, [NAME, CHROME + ' Headless']], [

                        / wv\).+(chrome)\/([\w\.]+)/i                                       // Chrome WebView
                    ], [[NAME, CHROME + ' WebView'], VERSION], [

                        /droid.+ version\/([\w\.]+)\b.+(?:mobile safari|safari)/i           // Android Browser
                    ], [VERSION, [NAME, 'Android ' + BROWSER]], [

                        /(chrome|omniweb|arora|[tizenoka]{5} ?browser)\/v?([\w\.]+)/i       // Chrome/OmniWeb/Arora/Tizen/Nokia
                    ], [NAME, VERSION], [

                        /version\/([\w\.\,]+) .*mobile\/\w+ (safari)/i                      // Mobile Safari
                    ], [VERSION, [NAME, 'Mobile Safari']], [
                        /version\/([\w(\.|\,)]+) .*(mobile ?safari|safari)/i                // Safari & Safari Mobile
                    ], [VERSION, NAME], [
                        /webkit.+?(mobile ?safari|safari)(\/[\w\.]+)/i                      // Safari < 3.0
                    ], [NAME, [VERSION, strMapper, oldSafariMap]], [

                        /(webkit|khtml)\/([\w\.]+)/i
                    ], [NAME, VERSION], [

                        // Gecko based
                        /(navigator|netscape\d?)\/([-\w\.]+)/i                              // Netscape
                    ], [[NAME, 'Netscape'], VERSION], [
                        /mobile vr; rv:([\w\.]+)\).+firefox/i                               // Firefox Reality
                    ], [VERSION, [NAME, FIREFOX + ' Reality']], [
                        /ekiohf.+(flow)\/([\w\.]+)/i,                                       // Flow
                        /(swiftfox)/i,                                                      // Swiftfox
                        /(icedragon|iceweasel|camino|chimera|fennec|maemo browser|minimo|conkeror|klar)[\/ ]?([\w\.\+]+)/i,
                        // IceDragon/Iceweasel/Camino/Chimera/Fennec/Maemo/Minimo/Conkeror/Klar
                        /(seamonkey|k-meleon|icecat|iceape|firebird|phoenix|palemoon|basilisk|waterfox)\/([-\w\.]+)$/i,
                        // Firefox/SeaMonkey/K-Meleon/IceCat/IceApe/Firebird/Phoenix
                        /(firefox)\/([\w\.]+)/i,                                            // Other Firefox-based
                        /(mozilla)\/([\w\.]+) .+rv\:.+gecko\/\d+/i,                         // Mozilla

                        // Other
                        /(polaris|lynx|dillo|icab|doris|amaya|w3m|netsurf|sleipnir|obigo|mosaic|(?:go|ice|up)[\. ]?browser)[-\/ ]?v?([\w\.]+)/i,
                        // Polaris/Lynx/Dillo/iCab/Doris/Amaya/w3m/NetSurf/Sleipnir/Obigo/Mosaic/Go/ICE/UP.Browser
                        /(links) \(([\w\.]+)/i                                              // Links
                    ], [NAME, VERSION]
                    ],

                    cpu: [[

                        /(?:(amd|x(?:(?:86|64)[-_])?|wow|win)64)[;\)]/i                     // AMD64 (x64)
                    ], [[ARCHITECTURE, 'amd64']], [

                        /(ia32(?=;))/i                                                      // IA32 (quicktime)
                    ], [[ARCHITECTURE, lowerize]], [

                        /((?:i[346]|x)86)[;\)]/i                                            // IA32 (x86)
                    ], [[ARCHITECTURE, 'ia32']], [

                        /\b(aarch64|arm(v?8e?l?|_?64))\b/i                                 // ARM64
                    ], [[ARCHITECTURE, 'arm64']], [

                        /\b(arm(?:v[67])?ht?n?[fl]p?)\b/i                                   // ARMHF
                    ], [[ARCHITECTURE, 'armhf']], [

                        // PocketPC mistakenly identified as PowerPC
                        /windows (ce|mobile); ppc;/i
                    ], [[ARCHITECTURE, 'arm']], [

                        /((?:ppc|powerpc)(?:64)?)(?: mac|;|\))/i                            // PowerPC
                    ], [[ARCHITECTURE, /ower/, EMPTY, lowerize]], [

                        /(sun4\w)[;\)]/i                                                    // SPARC
                    ], [[ARCHITECTURE, 'sparc']], [

                        /((?:avr32|ia64(?=;))|68k(?=\))|\barm(?=v(?:[1-7]|[5-7]1)l?|;|eabi)|(?=atmel )avr|(?:irix|mips|sparc)(?:64)?\b|pa-risc)/i
                        // IA64, 68K, ARM/64, AVR/32, IRIX/64, MIPS/64, SPARC/64, PA-RISC
                    ], [[ARCHITECTURE, lowerize]]
                    ],

                    device: [[

                        //////////////////////////
                        // MOBILES & TABLETS
                        // Ordered by popularity
                        /////////////////////////

                        // Samsung
                        /\b(sch-i[89]0\d|shw-m380s|sm-[ptx]\w{2,4}|gt-[pn]\d{2,4}|sgh-t8[56]9|nexus 10)/i
                    ], [MODEL, [VENDOR, SAMSUNG], [TYPE, TABLET]], [
                        /\b((?:s[cgp]h|gt|sm)-\w+|galaxy nexus)/i,
                        /samsung[- ]([-\w]+)/i,
                        /sec-(sgh\w+)/i
                    ], [MODEL, [VENDOR, SAMSUNG], [TYPE, MOBILE]], [

                        // Apple
                        /\((ip(?:hone|od)[\w ]*);/i                                         // iPod/iPhone
                    ], [MODEL, [VENDOR, APPLE], [TYPE, MOBILE]], [
                        /\((ipad);[-\w\),; ]+apple/i,                                       // iPad
                        /applecoremedia\/[\w\.]+ \((ipad)/i,
                        /\b(ipad)\d\d?,\d\d?[;\]].+ios/i
                    ], [MODEL, [VENDOR, APPLE], [TYPE, TABLET]], [

                        // Huawei
                        /\b((?:ag[rs][23]?|bah2?|sht?|btv)-a?[lw]\d{2})\b(?!.+d\/s)/i
                    ], [MODEL, [VENDOR, HUAWEI], [TYPE, TABLET]], [
                        /(?:huawei|honor)([-\w ]+)[;\)]/i,
                        /\b(nexus 6p|\w{2,4}e?-[atu]?[ln][\dx][012359c][adn]?)\b(?!.+d\/s)/i
                    ], [MODEL, [VENDOR, HUAWEI], [TYPE, MOBILE]], [

                        // Xiaomi
                        /\b(poco[\w ]+)(?: bui|\))/i,                                       // Xiaomi POCO
                        /\b; (\w+) build\/hm\1/i,                                           // Xiaomi Hongmi 'numeric' models
                        /\b(hm[-_ ]?note?[_ ]?(?:\d\w)?) bui/i,                             // Xiaomi Hongmi
                        /\b(redmi[\-_ ]?(?:note|k)?[\w_ ]+)(?: bui|\))/i,                   // Xiaomi Redmi
                        /\b(mi[-_ ]?(?:a\d|one|one[_ ]plus|note lte|max|cc)?[_ ]?(?:\d?\w?)[_ ]?(?:plus|se|lite)?)(?: bui|\))/i // Xiaomi Mi
                    ], [[MODEL, /_/g, ' '], [VENDOR, XIAOMI], [TYPE, MOBILE]], [
                        /\b(mi[-_ ]?(?:pad)(?:[\w_ ]+))(?: bui|\))/i                        // Mi Pad tablets
                    ], [[MODEL, /_/g, ' '], [VENDOR, XIAOMI], [TYPE, TABLET]], [

                        // OPPO
                        /; (\w+) bui.+ oppo/i,
                        /\b(cph[12]\d{3}|p(?:af|c[al]|d\w|e[ar])[mt]\d0|x9007|a101op)\b/i
                    ], [MODEL, [VENDOR, 'OPPO'], [TYPE, MOBILE]], [

                        // Vivo
                        /vivo (\w+)(?: bui|\))/i,
                        /\b(v[12]\d{3}\w?[at])(?: bui|;)/i
                    ], [MODEL, [VENDOR, 'Vivo'], [TYPE, MOBILE]], [

                        // Realme
                        /\b(rmx[12]\d{3})(?: bui|;|\))/i
                    ], [MODEL, [VENDOR, 'Realme'], [TYPE, MOBILE]], [

                        // Motorola
                        /\b(milestone|droid(?:[2-4x]| (?:bionic|x2|pro|razr))?:?( 4g)?)\b[\w ]+build\//i,
                        /\bmot(?:orola)?[- ](\w*)/i,
                        /((?:moto[\w\(\) ]+|xt\d{3,4}|nexus 6)(?= bui|\)))/i
                    ], [MODEL, [VENDOR, MOTOROLA], [TYPE, MOBILE]], [
                        /\b(mz60\d|xoom[2 ]{0,2}) build\//i
                    ], [MODEL, [VENDOR, MOTOROLA], [TYPE, TABLET]], [

                        // LG
                        /((?=lg)?[vl]k\-?\d{3}) bui| 3\.[-\w; ]{10}lg?-([06cv9]{3,4})/i
                    ], [MODEL, [VENDOR, LG], [TYPE, TABLET]], [
                        /(lm(?:-?f100[nv]?|-[\w\.]+)(?= bui|\))|nexus [45])/i,
                        /\blg[-e;\/ ]+((?!browser|netcast|android tv)\w+)/i,
                        /\blg-?([\d\w]+) bui/i
                    ], [MODEL, [VENDOR, LG], [TYPE, MOBILE]], [

                        // Lenovo
                        /(ideatab[-\w ]+)/i,
                        /lenovo ?(s[56]000[-\w]+|tab(?:[\w ]+)|yt[-\d\w]{6}|tb[-\d\w]{6})/i
                    ], [MODEL, [VENDOR, 'Lenovo'], [TYPE, TABLET]], [

                        // Nokia
                        /(?:maemo|nokia).*(n900|lumia \d+)/i,
                        /nokia[-_ ]?([-\w\.]*)/i
                    ], [[MODEL, /_/g, ' '], [VENDOR, 'Nokia'], [TYPE, MOBILE]], [

                        // Google
                        /(pixel c)\b/i                                                      // Google Pixel C
                    ], [MODEL, [VENDOR, GOOGLE], [TYPE, TABLET]], [
                        /droid.+; (pixel[\daxl ]{0,6})(?: bui|\))/i                         // Google Pixel
                    ], [MODEL, [VENDOR, GOOGLE], [TYPE, MOBILE]], [

                        // Sony
                        /droid.+ (a?\d[0-2]{2}so|[c-g]\d{4}|so[-gl]\w+|xq-a\w[4-7][12])(?= bui|\).+chrome\/(?![1-6]{0,1}\d\.))/i
                    ], [MODEL, [VENDOR, SONY], [TYPE, MOBILE]], [
                        /sony tablet [ps]/i,
                        /\b(?:sony)?sgp\w+(?: bui|\))/i
                    ], [[MODEL, 'Xperia Tablet'], [VENDOR, SONY], [TYPE, TABLET]], [

                        // OnePlus
                        / (kb2005|in20[12]5|be20[12][59])\b/i,
                        /(?:one)?(?:plus)? (a\d0\d\d)(?: b|\))/i
                    ], [MODEL, [VENDOR, 'OnePlus'], [TYPE, MOBILE]], [

                        // Amazon
                        /(alexa)webm/i,
                        /(kf[a-z]{2}wi)( bui|\))/i,                                         // Kindle Fire without Silk
                        /(kf[a-z]+)( bui|\)).+silk\//i                                      // Kindle Fire HD
                    ], [MODEL, [VENDOR, AMAZON], [TYPE, TABLET]], [
                        /((?:sd|kf)[0349hijorstuw]+)( bui|\)).+silk\//i                     // Fire Phone
                    ], [[MODEL, /(.+)/g, 'Fire Phone $1'], [VENDOR, AMAZON], [TYPE, MOBILE]], [

                        // BlackBerry
                        /(playbook);[-\w\),; ]+(rim)/i                                      // BlackBerry PlayBook
                    ], [MODEL, VENDOR, [TYPE, TABLET]], [
                        /\b((?:bb[a-f]|st[hv])100-\d)/i,
                        /\(bb10; (\w+)/i                                                    // BlackBerry 10
                    ], [MODEL, [VENDOR, BLACKBERRY], [TYPE, MOBILE]], [

                        // Asus
                        /(?:\b|asus_)(transfo[prime ]{4,10} \w+|eeepc|slider \w+|nexus 7|padfone|p00[cj])/i
                    ], [MODEL, [VENDOR, ASUS], [TYPE, TABLET]], [
                        / (z[bes]6[027][012][km][ls]|zenfone \d\w?)\b/i
                    ], [MODEL, [VENDOR, ASUS], [TYPE, MOBILE]], [

                        // HTC
                        /(nexus 9)/i                                                        // HTC Nexus 9
                    ], [MODEL, [VENDOR, 'HTC'], [TYPE, TABLET]], [
                        /(htc)[-;_ ]{1,2}([\w ]+(?=\)| bui)|\w+)/i,                         // HTC

                        // ZTE
                        /(zte)[- ]([\w ]+?)(?: bui|\/|\))/i,
                        /(alcatel|geeksphone|nexian|panasonic|sony(?!-bra))[-_ ]?([-\w]*)/i         // Alcatel/GeeksPhone/Nexian/Panasonic/Sony
                    ], [VENDOR, [MODEL, /_/g, ' '], [TYPE, MOBILE]], [

                        // Acer
                        /droid.+; ([ab][1-7]-?[0178a]\d\d?)/i
                    ], [MODEL, [VENDOR, 'Acer'], [TYPE, TABLET]], [

                        // Meizu
                        /droid.+; (m[1-5] note) bui/i,
                        /\bmz-([-\w]{2,})/i
                    ], [MODEL, [VENDOR, 'Meizu'], [TYPE, MOBILE]], [

                        // Sharp
                        /\b(sh-?[altvz]?\d\d[a-ekm]?)/i
                    ], [MODEL, [VENDOR, SHARP], [TYPE, MOBILE]], [

                        // MIXED
                        /(blackberry|benq|palm(?=\-)|sonyericsson|acer|asus|dell|meizu|motorola|polytron)[-_ ]?([-\w]*)/i,
                        // BlackBerry/BenQ/Palm/Sony-Ericsson/Acer/Asus/Dell/Meizu/Motorola/Polytron
                        /(hp) ([\w ]+\w)/i,                                                 // HP iPAQ
                        /(asus)-?(\w+)/i,                                                   // Asus
                        /(microsoft); (lumia[\w ]+)/i,                                      // Microsoft Lumia
                        /(lenovo)[-_ ]?([-\w]+)/i,                                          // Lenovo
                        /(jolla)/i,                                                         // Jolla
                        /(oppo) ?([\w ]+) bui/i                                             // OPPO
                    ], [VENDOR, MODEL, [TYPE, MOBILE]], [

                        /(archos) (gamepad2?)/i,                                            // Archos
                        /(hp).+(touchpad(?!.+tablet)|tablet)/i,                             // HP TouchPad
                        /(kindle)\/([\w\.]+)/i,                                             // Kindle
                        /(nook)[\w ]+build\/(\w+)/i,                                        // Nook
                        /(dell) (strea[kpr\d ]*[\dko])/i,                                   // Dell Streak
                        /(le[- ]+pan)[- ]+(\w{1,9}) bui/i,                                  // Le Pan Tablets
                        /(trinity)[- ]*(t\d{3}) bui/i,                                      // Trinity Tablets
                        /(gigaset)[- ]+(q\w{1,9}) bui/i,                                    // Gigaset Tablets
                        /(vodafone) ([\w ]+)(?:\)| bui)/i                                   // Vodafone
                    ], [VENDOR, MODEL, [TYPE, TABLET]], [

                        /(surface duo)/i                                                    // Surface Duo
                    ], [MODEL, [VENDOR, MICROSOFT], [TYPE, TABLET]], [
                        /droid [\d\.]+; (fp\du?)(?: b|\))/i                                 // Fairphone
                    ], [MODEL, [VENDOR, 'Fairphone'], [TYPE, MOBILE]], [
                        /(u304aa)/i                                                         // AT&T
                    ], [MODEL, [VENDOR, 'AT&T'], [TYPE, MOBILE]], [
                        /\bsie-(\w*)/i                                                      // Siemens
                    ], [MODEL, [VENDOR, 'Siemens'], [TYPE, MOBILE]], [
                        /\b(rct\w+) b/i                                                     // RCA Tablets
                    ], [MODEL, [VENDOR, 'RCA'], [TYPE, TABLET]], [
                        /\b(venue[\d ]{2,7}) b/i                                            // Dell Venue Tablets
                    ], [MODEL, [VENDOR, 'Dell'], [TYPE, TABLET]], [
                        /\b(q(?:mv|ta)\w+) b/i                                              // Verizon Tablet
                    ], [MODEL, [VENDOR, 'Verizon'], [TYPE, TABLET]], [
                        /\b(?:barnes[& ]+noble |bn[rt])([\w\+ ]*) b/i                       // Barnes & Noble Tablet
                    ], [MODEL, [VENDOR, 'Barnes & Noble'], [TYPE, TABLET]], [
                        /\b(tm\d{3}\w+) b/i
                    ], [MODEL, [VENDOR, 'NuVision'], [TYPE, TABLET]], [
                        /\b(k88) b/i                                                        // ZTE K Series Tablet
                    ], [MODEL, [VENDOR, 'ZTE'], [TYPE, TABLET]], [
                        /\b(nx\d{3}j) b/i                                                   // ZTE Nubia
                    ], [MODEL, [VENDOR, 'ZTE'], [TYPE, MOBILE]], [
                        /\b(gen\d{3}) b.+49h/i                                              // Swiss GEN Mobile
                    ], [MODEL, [VENDOR, 'Swiss'], [TYPE, MOBILE]], [
                        /\b(zur\d{3}) b/i                                                   // Swiss ZUR Tablet
                    ], [MODEL, [VENDOR, 'Swiss'], [TYPE, TABLET]], [
                        /\b((zeki)?tb.*\b) b/i                                              // Zeki Tablets
                    ], [MODEL, [VENDOR, 'Zeki'], [TYPE, TABLET]], [
                        /\b([yr]\d{2}) b/i,
                        /\b(dragon[- ]+touch |dt)(\w{5}) b/i                                // Dragon Touch Tablet
                    ], [[VENDOR, 'Dragon Touch'], MODEL, [TYPE, TABLET]], [
                        /\b(ns-?\w{0,9}) b/i                                                // Insignia Tablets
                    ], [MODEL, [VENDOR, 'Insignia'], [TYPE, TABLET]], [
                        /\b((nxa|next)-?\w{0,9}) b/i                                        // NextBook Tablets
                    ], [MODEL, [VENDOR, 'NextBook'], [TYPE, TABLET]], [
                        /\b(xtreme\_)?(v(1[045]|2[015]|[3469]0|7[05])) b/i                  // Voice Xtreme Phones
                    ], [[VENDOR, 'Voice'], MODEL, [TYPE, MOBILE]], [
                        /\b(lvtel\-)?(v1[12]) b/i                                           // LvTel Phones
                    ], [[VENDOR, 'LvTel'], MODEL, [TYPE, MOBILE]], [
                        /\b(ph-1) /i                                                        // Essential PH-1
                    ], [MODEL, [VENDOR, 'Essential'], [TYPE, MOBILE]], [
                        /\b(v(100md|700na|7011|917g).*\b) b/i                               // Envizen Tablets
                    ], [MODEL, [VENDOR, 'Envizen'], [TYPE, TABLET]], [
                        /\b(trio[-\w\. ]+) b/i                                              // MachSpeed Tablets
                    ], [MODEL, [VENDOR, 'MachSpeed'], [TYPE, TABLET]], [
                        /\btu_(1491) b/i                                                    // Rotor Tablets
                    ], [MODEL, [VENDOR, 'Rotor'], [TYPE, TABLET]], [
                        /(shield[\w ]+) b/i                                                 // Nvidia Shield Tablets
                    ], [MODEL, [VENDOR, 'Nvidia'], [TYPE, TABLET]], [
                        /(sprint) (\w+)/i                                                   // Sprint Phones
                    ], [VENDOR, MODEL, [TYPE, MOBILE]], [
                        /(kin\.[onetw]{3})/i                                                // Microsoft Kin
                    ], [[MODEL, /\./g, ' '], [VENDOR, MICROSOFT], [TYPE, MOBILE]], [
                        /droid.+; (cc6666?|et5[16]|mc[239][23]x?|vc8[03]x?)\)/i             // Zebra
                    ], [MODEL, [VENDOR, ZEBRA], [TYPE, TABLET]], [
                        /droid.+; (ec30|ps20|tc[2-8]\d[kx])\)/i
                    ], [MODEL, [VENDOR, ZEBRA], [TYPE, MOBILE]], [

                        ///////////////////
                        // CONSOLES
                        ///////////////////

                        /(ouya)/i,                                                          // Ouya
                        /(nintendo) ([wids3utch]+)/i                                        // Nintendo
                    ], [VENDOR, MODEL, [TYPE, CONSOLE]], [
                        /droid.+; (shield) bui/i                                            // Nvidia
                    ], [MODEL, [VENDOR, 'Nvidia'], [TYPE, CONSOLE]], [
                        /(playstation [345portablevi]+)/i                                   // Playstation
                    ], [MODEL, [VENDOR, SONY], [TYPE, CONSOLE]], [
                        /\b(xbox(?: one)?(?!; xbox))[\); ]/i                                // Microsoft Xbox
                    ], [MODEL, [VENDOR, MICROSOFT], [TYPE, CONSOLE]], [

                        ///////////////////
                        // SMARTTVS
                        ///////////////////

                        /smart-tv.+(samsung)/i                                              // Samsung
                    ], [VENDOR, [TYPE, SMARTTV]], [
                        /hbbtv.+maple;(\d+)/i
                    ], [[MODEL, /^/, 'SmartTV'], [VENDOR, SAMSUNG], [TYPE, SMARTTV]], [
                        /(nux; netcast.+smarttv|lg (netcast\.tv-201\d|android tv))/i        // LG SmartTV
                    ], [[VENDOR, LG], [TYPE, SMARTTV]], [
                        /(apple) ?tv/i                                                      // Apple TV
                    ], [VENDOR, [MODEL, APPLE + ' TV'], [TYPE, SMARTTV]], [
                        /crkey/i                                                            // Google Chromecast
                    ], [[MODEL, CHROME + 'cast'], [VENDOR, GOOGLE], [TYPE, SMARTTV]], [
                        /droid.+aft(\w)( bui|\))/i                                          // Fire TV
                    ], [MODEL, [VENDOR, AMAZON], [TYPE, SMARTTV]], [
                        /\(dtv[\);].+(aquos)/i,
                        /(aquos-tv[\w ]+)\)/i                                               // Sharp
                    ], [MODEL, [VENDOR, SHARP], [TYPE, SMARTTV]], [
                        /(bravia[\w ]+)( bui|\))/i                                              // Sony
                    ], [MODEL, [VENDOR, SONY], [TYPE, SMARTTV]], [
                        /(mitv-\w{5}) bui/i                                                 // Xiaomi
                    ], [MODEL, [VENDOR, XIAOMI], [TYPE, SMARTTV]], [
                        /\b(roku)[\dx]*[\)\/]((?:dvp-)?[\d\.]*)/i,                          // Roku
                        /hbbtv\/\d+\.\d+\.\d+ +\([\w ]*; *(\w[^;]*);([^;]*)/i               // HbbTV devices
                    ], [[VENDOR, trim], [MODEL, trim], [TYPE, SMARTTV]], [
                        /\b(android tv|smart[- ]?tv|opera tv|tv; rv:)\b/i                   // SmartTV from Unidentified Vendors
                    ], [[TYPE, SMARTTV]], [

                        ///////////////////
                        // WEARABLES
                        ///////////////////

                        /((pebble))app/i                                                    // Pebble
                    ], [VENDOR, MODEL, [TYPE, WEARABLE]], [
                        /droid.+; (glass) \d/i                                              // Google Glass
                    ], [MODEL, [VENDOR, GOOGLE], [TYPE, WEARABLE]], [
                        /droid.+; (wt63?0{2,3})\)/i
                    ], [MODEL, [VENDOR, ZEBRA], [TYPE, WEARABLE]], [
                        /(quest( 2)?)/i                                                     // Oculus Quest
                    ], [MODEL, [VENDOR, FACEBOOK], [TYPE, WEARABLE]], [

                        ///////////////////
                        // EMBEDDED
                        ///////////////////

                        /(tesla)(?: qtcarbrowser|\/[-\w\.]+)/i                              // Tesla
                    ], [VENDOR, [TYPE, EMBEDDED]], [

                        ////////////////////
                        // MIXED (GENERIC)
                        ///////////////////

                        /droid .+?; ([^;]+?)(?: bui|\) applew).+? mobile safari/i           // Android Phones from Unidentified Vendors
                    ], [MODEL, [TYPE, MOBILE]], [
                        /droid .+?; ([^;]+?)(?: bui|\) applew).+?(?! mobile) safari/i       // Android Tablets from Unidentified Vendors
                    ], [MODEL, [TYPE, TABLET]], [
                        /\b((tablet|tab)[;\/]|focus\/\d(?!.+mobile))/i                      // Unidentifiable Tablet
                    ], [[TYPE, TABLET]], [
                        /(phone|mobile(?:[;\/]| [ \w\/\.]*safari)|pda(?=.+windows ce))/i    // Unidentifiable Mobile
                    ], [[TYPE, MOBILE]], [
                        /(android[-\w\. ]{0,9});.+buil/i                                    // Generic Android Device
                    ], [MODEL, [VENDOR, 'Generic']]
                    ],

                    engine: [[

                        /windows.+ edge\/([\w\.]+)/i                                       // EdgeHTML
                    ], [VERSION, [NAME, EDGE + 'HTML']], [

                        /webkit\/537\.36.+chrome\/(?!27)([\w\.]+)/i                         // Blink
                    ], [VERSION, [NAME, 'Blink']], [

                        /(presto)\/([\w\.]+)/i,                                             // Presto
                        /(webkit|trident|netfront|netsurf|amaya|lynx|w3m|goanna)\/([\w\.]+)/i, // WebKit/Trident/NetFront/NetSurf/Amaya/Lynx/w3m/Goanna
                        /ekioh(flow)\/([\w\.]+)/i,                                          // Flow
                        /(khtml|tasman|links)[\/ ]\(?([\w\.]+)/i,                           // KHTML/Tasman/Links
                        /(icab)[\/ ]([23]\.[\d\.]+)/i                                       // iCab
                    ], [NAME, VERSION], [

                        /rv\:([\w\.]{1,9})\b.+(gecko)/i                                     // Gecko
                    ], [VERSION, NAME]
                    ],

                    os: [[

                        // Windows
                        /microsoft (windows) (vista|xp)/i                                   // Windows (iTunes)
                    ], [NAME, VERSION], [
                        /(windows) nt 6\.2; (arm)/i,                                        // Windows RT
                        /(windows (?:phone(?: os)?|mobile))[\/ ]?([\d\.\w ]*)/i,            // Windows Phone
                        /(windows)[\/ ]?([ntce\d\. ]+\w)(?!.+xbox)/i
                    ], [NAME, [VERSION, strMapper, windowsVersionMap]], [
                        /(win(?=3|9|n)|win 9x )([nt\d\.]+)/i
                    ], [[NAME, 'Windows'], [VERSION, strMapper, windowsVersionMap]], [

                        // iOS/macOS
                        /ip[honead]{2,4}\b(?:.*os ([\w]+) like mac|; opera)/i,              // iOS
                        /cfnetwork\/.+darwin/i
                    ], [[VERSION, /_/g, '.'], [NAME, 'iOS']], [
                        /(mac os x) ?([\w\. ]*)/i,
                        /(macintosh|mac_powerpc\b)(?!.+haiku)/i                             // Mac OS
                    ], [[NAME, 'Mac OS'], [VERSION, /_/g, '.']], [

                        // Mobile OSes
                        /droid ([\w\.]+)\b.+(android[- ]x86|harmonyos)/i                    // Android-x86/HarmonyOS
                    ], [VERSION, NAME], [                                               // Android/WebOS/QNX/Bada/RIM/Maemo/MeeGo/Sailfish OS
                        /(android|webos|qnx|bada|rim tablet os|maemo|meego|sailfish)[-\/ ]?([\w\.]*)/i,
                        /(blackberry)\w*\/([\w\.]*)/i,                                      // Blackberry
                        /(tizen|kaios)[\/ ]([\w\.]+)/i,                                     // Tizen/KaiOS
                        /\((series40);/i                                                    // Series 40
                    ], [NAME, VERSION], [
                        /\(bb(10);/i                                                        // BlackBerry 10
                    ], [VERSION, [NAME, BLACKBERRY]], [
                        /(?:symbian ?os|symbos|s60(?=;)|series60)[-\/ ]?([\w\.]*)/i         // Symbian
                    ], [VERSION, [NAME, 'Symbian']], [
                        /mozilla\/[\d\.]+ \((?:mobile|tablet|tv|mobile; [\w ]+); rv:.+ gecko\/([\w\.]+)/i // Firefox OS
                    ], [VERSION, [NAME, FIREFOX + ' OS']], [
                        /web0s;.+rt(tv)/i,
                        /\b(?:hp)?wos(?:browser)?\/([\w\.]+)/i                              // WebOS
                    ], [VERSION, [NAME, 'webOS']], [

                        // Google Chromecast
                        /crkey\/([\d\.]+)/i                                                 // Google Chromecast
                    ], [VERSION, [NAME, CHROME + 'cast']], [
                        /(cros) [\w]+ ([\w\.]+\w)/i                                         // Chromium OS
                    ], [[NAME, 'Chromium OS'], VERSION], [

                        // Console
                        /(nintendo|playstation) ([wids345portablevuch]+)/i,                 // Nintendo/Playstation
                        /(xbox); +xbox ([^\);]+)/i,                                         // Microsoft Xbox (360, One, X, S, Series X, Series S)

                        // Other
                        /\b(joli|palm)\b ?(?:os)?\/?([\w\.]*)/i,                            // Joli/Palm
                        /(mint)[\/\(\) ]?(\w*)/i,                                           // Mint
                        /(mageia|vectorlinux)[; ]/i,                                        // Mageia/VectorLinux
                        /([kxln]?ubuntu|debian|suse|opensuse|gentoo|arch(?= linux)|slackware|fedora|mandriva|centos|pclinuxos|red ?hat|zenwalk|linpus|raspbian|plan 9|minix|risc os|contiki|deepin|manjaro|elementary os|sabayon|linspire)(?: gnu\/linux)?(?: enterprise)?(?:[- ]linux)?(?:-gnu)?[-\/ ]?(?!chrom|package)([-\w\.]*)/i,
                        // Ubuntu/Debian/SUSE/Gentoo/Arch/Slackware/Fedora/Mandriva/CentOS/PCLinuxOS/RedHat/Zenwalk/Linpus/Raspbian/Plan9/Minix/RISCOS/Contiki/Deepin/Manjaro/elementary/Sabayon/Linspire
                        /(hurd|linux) ?([\w\.]*)/i,                                         // Hurd/Linux
                        /(gnu) ?([\w\.]*)/i,                                                // GNU
                        /\b([-frentopcghs]{0,5}bsd|dragonfly)[\/ ]?(?!amd|[ix346]{1,2}86)([\w\.]*)/i, // FreeBSD/NetBSD/OpenBSD/PC-BSD/GhostBSD/DragonFly
                        /(haiku) (\w+)/i                                                    // Haiku
                    ], [NAME, VERSION], [
                        /(sunos) ?([\w\.\d]*)/i                                             // Solaris
                    ], [[NAME, 'Solaris'], VERSION], [
                        /((?:open)?solaris)[-\/ ]?([\w\.]*)/i,                              // Solaris
                        /(aix) ((\d)(?=\.|\)| )[\w\.])*/i,                                  // AIX
                        /\b(beos|os\/2|amigaos|morphos|openvms|fuchsia|hp-ux)/i,            // BeOS/OS2/AmigaOS/MorphOS/OpenVMS/Fuchsia/HP-UX
                        /(unix) ?([\w\.]*)/i                                                // UNIX
                    ], [NAME, VERSION]
                    ]
                }

                /////////////////
                // Constructor
                ////////////////

                var UAParser = function (ua, extensions) {

                    if (typeof ua === OBJ_TYPE) {
                        extensions = ua
                        ua = undefined
                    }

                    if (!(this instanceof UAParser)) {
                        return new UAParser(ua, extensions).getResult()
                    }

                    var _ua = ua || ((typeof window !== UNDEF_TYPE && window.navigator && window.navigator.userAgent) ? window.navigator.userAgent : EMPTY)
                    var _rgxmap = extensions ? extend(regexes, extensions) : regexes

                    this.getBrowser = function () {
                        var _browser = {}
                        _browser[NAME] = undefined
                        _browser[VERSION] = undefined
                        rgxMapper.call(_browser, _ua, _rgxmap.browser)
                        _browser.major = majorize(_browser.version)
                        return _browser
                    }
                    this.getCPU = function () {
                        var _cpu = {}
                        _cpu[ARCHITECTURE] = undefined
                        rgxMapper.call(_cpu, _ua, _rgxmap.cpu)
                        return _cpu
                    }
                    this.getDevice = function () {
                        var _device = {}
                        _device[VENDOR] = undefined
                        _device[MODEL] = undefined
                        _device[TYPE] = undefined
                        rgxMapper.call(_device, _ua, _rgxmap.device)
                        return _device
                    }
                    this.getEngine = function () {
                        var _engine = {}
                        _engine[NAME] = undefined
                        _engine[VERSION] = undefined
                        rgxMapper.call(_engine, _ua, _rgxmap.engine)
                        return _engine
                    }
                    this.getOS = function () {
                        var _os = {}
                        _os[NAME] = undefined
                        _os[VERSION] = undefined
                        rgxMapper.call(_os, _ua, _rgxmap.os)
                        return _os
                    }
                    this.getResult = function () {
                        return {
                            ua: this.getUA(),
                            browser: this.getBrowser(),
                            engine: this.getEngine(),
                            os: this.getOS(),
                            device: this.getDevice(),
                            cpu: this.getCPU()
                        }
                    }
                    this.getUA = function () {
                        return _ua
                    }
                    this.setUA = function (ua) {
                        _ua = (typeof ua === STR_TYPE && ua.length > UA_MAX_LENGTH) ? trim(ua, UA_MAX_LENGTH) : ua
                        return this
                    }
                    this.setUA(_ua)
                    return this
                }

                UAParser.VERSION = LIBVERSION
                UAParser.BROWSER = enumerize([NAME, VERSION, MAJOR])
                UAParser.CPU = enumerize([ARCHITECTURE])
                UAParser.DEVICE = enumerize([MODEL, VENDOR, TYPE, CONSOLE, MOBILE, SMARTTV, TABLET, WEARABLE, EMBEDDED])
                UAParser.ENGINE = UAParser.OS = enumerize([NAME, VERSION])

                ///////////
                // Export
                //////////

                // check js environment
                if (typeof (exports) !== UNDEF_TYPE) {
                    // nodejs env
                    if (typeof module !== UNDEF_TYPE && module.exports) {
                        exports = module.exports = UAParser
                    }
                    exports.UAParser = UAParser
                } else {
                    // requirejs env (optional)
                    if (typeof (define) === FUNC_TYPE && define.amd) {
                        define(function () {
                            return UAParser
                        })
                    } else if (typeof window !== UNDEF_TYPE) {
                        // browser env
                        window.UAParser = UAParser
                    }
                }

                // jQuery/Zepto specific (optional)
                // Note:
                //   In AMD env the global scope should be kept clean, but jQuery is an exception.
                //   jQuery always exports to global scope, unless jQuery.noConflict(true) is used,
                //   and we should catch that.
                var $ = typeof window !== UNDEF_TYPE && (window.jQuery || window.Zepto)
                if ($ && !$.ua) {
                    var parser = new UAParser()
                    $.ua = parser.getResult()
                    $.ua.get = function () {
                        return parser.getUA()
                    }
                    $.ua.set = function (ua) {
                        parser.setUA(ua)
                        var result = parser.getResult()
                        for (var prop in result) {
                            $.ua[prop] = result[prop]
                        }
                    }
                }

            })(typeof window === 'object' ? window : this)

        }, {}],
        22: [function (require, module, exports) {
            /**
             * Convert array of 16 byte values to UUID string format of the form:
             * XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
             */
            var byteToHex = []
            for (var i = 0; i < 256; ++i) {
                byteToHex[i] = (i + 0x100).toString(16).substr(1)
            }

            function bytesToUuid(buf, offset) {
                var i = offset || 0
                var bth = byteToHex
                // join used to fix memory issue caused by concatenation: https://bugs.chromium.org/p/v8/issues/detail?id=3175#c4
                return ([
                    bth[buf[i++]], bth[buf[i++]],
                    bth[buf[i++]], bth[buf[i++]], '-',
                    bth[buf[i++]], bth[buf[i++]], '-',
                    bth[buf[i++]], bth[buf[i++]], '-',
                    bth[buf[i++]], bth[buf[i++]], '-',
                    bth[buf[i++]], bth[buf[i++]],
                    bth[buf[i++]], bth[buf[i++]],
                    bth[buf[i++]], bth[buf[i++]]
                ]).join('')
            }

            module.exports = bytesToUuid

        }, {}],
        23: [function (require, module, exports) {
// Unique ID creation requires a high quality random # generator.  In the
// browser this is a little complicated due to unknown quality of Math.random()
// and inconsistent support for the `crypto` API.  We do the best we can via
// feature-detection

// getRandomValues needs to be invoked in a context where "this" is a Crypto
// implementation. Also, find the complete implementation of crypto on IE11.
            var getRandomValues = (typeof (crypto) != 'undefined' && crypto.getRandomValues && crypto.getRandomValues.bind(crypto)) ||
                (typeof (msCrypto) != 'undefined' && typeof window.msCrypto.getRandomValues == 'function' && msCrypto.getRandomValues.bind(msCrypto))

            if (getRandomValues) {
                // WHATWG crypto RNG - http://wiki.whatwg.org/wiki/Crypto
                var rnds8 = new Uint8Array(16) // eslint-disable-line no-undef

                module.exports = function whatwgRNG() {
                    getRandomValues(rnds8)
                    return rnds8
                }
            } else {
                // Math.random()-based (RNG)
                //
                // If all else fails, use Math.random().  It's fast, but is of unspecified
                // quality.
                var rnds = new Array(16)

                module.exports = function mathRNG() {
                    for (var i = 0, r; i < 16; i++) {
                        if ((i & 0x03) === 0) r = Math.random() * 0x100000000
                        rnds[i] = r >>> ((i & 0x03) << 3) & 0xff
                    }

                    return rnds
                }
            }

        }, {}],
        24: [function (require, module, exports) {
            var rng = require('./lib/rng')
            var bytesToUuid = require('./lib/bytesToUuid')

            function v4(options, buf, offset) {
                var i = buf && offset || 0

                if (typeof (options) == 'string') {
                    buf = options === 'binary' ? new Array(16) : null
                    options = null
                }
                options = options || {}

                var rnds = options.random || (options.rng || rng)()

                // Per 4.4, set bits for version and `clock_seq_hi_and_reserved`
                rnds[6] = (rnds[6] & 0x0f) | 0x40
                rnds[8] = (rnds[8] & 0x3f) | 0x80

                // Copy bytes to buffer, if provided
                if (buf) {
                    for (var ii = 0; ii < 16; ++ii) {
                        buf[i + ii] = rnds[ii]
                    }
                }

                return buf || bytesToUuid(rnds)
            }

            module.exports = v4

        }, {'./lib/bytesToUuid': 22, './lib/rng': 23}],
        25: [function (require, module, exports) {
            /*
WildEmitter.js is a slim little event emitter by @henrikjoreteg largely based
on @visionmedia's Emitter from UI Kit.

Why? I wanted it standalone.

I also wanted support for wildcard emitters like this:

emitter.on('*', function (eventName, other, event, payloads) {

});

emitter.on('somenamespace*', function (eventName, payloads) {

});

Please note that callbacks triggered by wildcard registered events also get
the event name as the first argument.
*/

            module.exports = WildEmitter

            function WildEmitter() {
            }

            WildEmitter.mixin = function (constructor) {
                var prototype = constructor.prototype || constructor

                prototype.isWildEmitter = true

                // Listen on the given `event` with `fn`. Store a group name if present.
                prototype.on = function (event, groupName, fn) {
                    this.callbacks = this.callbacks || {}
                    var hasGroup = (arguments.length === 3),
                        group = hasGroup ? arguments[1] : undefined,
                        func = hasGroup ? arguments[2] : arguments[1]
                    func._groupName = group;
                    (this.callbacks[event] = this.callbacks[event] || []).push(func)
                    return this
                }

                // Adds an `event` listener that will be invoked a single
                // time then automatically removed.
                prototype.once = function (event, groupName, fn) {
                    var self = this,
                        hasGroup = (arguments.length === 3),
                        group = hasGroup ? arguments[1] : undefined,
                        func = hasGroup ? arguments[2] : arguments[1]

                    function on() {
                        self.off(event, on)
                        func.apply(this, arguments)
                    }

                    this.on(event, group, on)
                    return this
                }

                // Unbinds an entire group
                prototype.releaseGroup = function (groupName) {
                    this.callbacks = this.callbacks || {}
                    var item, i, len, handlers
                    for (item in this.callbacks) {
                        handlers = this.callbacks[item]
                        for (i = 0, len = handlers.length; i < len; i++) {
                            if (handlers[i]._groupName === groupName) {
                                //console.log('removing');
                                // remove it and shorten the array we're looping through
                                handlers.splice(i, 1)
                                i--
                                len--
                            }
                        }
                    }
                    return this
                }

                // Remove the given callback for `event` or all
                // registered callbacks.
                prototype.off = function (event, fn) {
                    this.callbacks = this.callbacks || {}
                    var callbacks = this.callbacks[event],
                        i

                    if (!callbacks) return this

                    // remove all handlers
                    if (arguments.length === 1) {
                        delete this.callbacks[event]
                        return this
                    }

                    // remove specific handler
                    i = callbacks.indexOf(fn)
                    if (i !== -1) {
                        callbacks.splice(i, 1)
                        if (callbacks.length === 0) {
                            delete this.callbacks[event]
                        }
                    }
                    return this
                }

                /// Emit `event` with the given args.
                // also calls any `*` handlers
                prototype.emit = function (event) {
                    this.callbacks = this.callbacks || {}
                    var args = [].slice.call(arguments, 1),
                        callbacks = this.callbacks[event],
                        specialCallbacks = this.getWildcardCallbacks(event),
                        i,
                        len,
                        item,
                        listeners

                    if (callbacks) {
                        listeners = callbacks.slice()
                        for (i = 0, len = listeners.length; i < len; ++i) {
                            if (!listeners[i]) {
                                break
                            }
                            listeners[i].apply(this, args)
                        }
                    }

                    if (specialCallbacks) {
                        len = specialCallbacks.length
                        listeners = specialCallbacks.slice()
                        for (i = 0, len = listeners.length; i < len; ++i) {
                            if (!listeners[i]) {
                                break
                            }
                            listeners[i].apply(this, [event].concat(args))
                        }
                    }

                    return this
                }

                // Helper for for finding special wildcard event handlers that match the event
                prototype.getWildcardCallbacks = function (eventName) {
                    this.callbacks = this.callbacks || {}
                    var item,
                        split,
                        result = []

                    for (item in this.callbacks) {
                        split = item.split('*')
                        if (item === '*' || (split.length === 2 && eventName.slice(0, split[0].length) === split[0])) {
                            result = result.concat(this.callbacks[item])
                        }
                    }
                    return result
                }

            }

            WildEmitter.mixin(WildEmitter)

        }, {}]
    }, {}, [2])(2)
})
